diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java index 03088db..f72f8ca 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java @@ -58,18 +58,18 @@ public class AuthAdminController { return ApiResponse.success(); } - @PostMapping("/roles/{roleCode}/full-accounts/{uid}/change-password") - @Operation(summary = "管理员修改 FULL 账户口令", description = "管理员为目标角色指定 UKey 席位账号设置新口令。") + @PostMapping("/roles/{roleCode}/accounts/{uid}/change-password") + @Operation(summary = "管理员修改角色席位口令", description = "管理员为目标角色指定席位账号设置新口令。") @ReplayProtected -// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 FULL 账户口令") - public ApiResponse changeFullAccountPassword( + @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) + @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改角色席位口令") + public ApiResponse changeAccountPassword( @PathVariable("roleCode") String roleCode, @PathVariable("uid") Integer uid, @Valid @RequestBody AdminChangePasswordRequest request, HttpServletRequest httpRequest ) { - authAdminService.changeFullAccountPassword( + authAdminService.changeAccountPassword( (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), roleCode, @@ -80,28 +80,6 @@ public class AuthAdminController { return ApiResponse.success(); } - @PostMapping("/roles/{roleCode}/limited-accounts/{username}/change-password") - @Operation(summary = "管理员修改 LIMITED 账户口令", description = "管理员为目标角色指定独立用户账号设置新口令。") - @ReplayProtected -// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 LIMITED 账户口令") - public ApiResponse changeLimitedAccountPassword( - @PathVariable("roleCode") String roleCode, - @PathVariable("username") String username, - @Valid @RequestBody AdminChangePasswordRequest request, - HttpServletRequest httpRequest - ) { - authAdminService.changeLimitedAccountPassword( - (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), - (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), - roleCode, - username, - request.getOldPassword(), - request.getNewPassword() - ); - return ApiResponse.success(); - } - @PostMapping("/super-admin/ukeys/issue-sign") @Operation(summary = "签发并绑定超级管理员 UKey", description = "系统初始化后登记超级管理员 UKey,不要求已有登录态。") public ApiResponse issueSuperAdminUkeyBindingSign( diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index bdd63b9..f3cd519 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -65,16 +65,16 @@ public class AuthController { return ApiResponse.success(); } - @PostMapping("/full-accounts/{uid}/change-password") - @Operation(summary = "修改当前 FULL 账户口令", description = "基于当前 FULL 会话校验并更新指定席位账户口令。") - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 FULL 账户口令") + @PostMapping("/accounts/{uid}/change-password") + @Operation(summary = "修改当前登录席位口令", description = "基于当前会话校验并更新指定角色席位口令。") + @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前登录席位口令") @ReplayProtected - public ApiResponse changeFullAccountPassword( + public ApiResponse changeAccountPassword( @PathVariable("uid") Integer uid, @Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest ) { - authService.changeFullAccountPassword( + authService.changeAccountPassword( (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), uid, request.getCurrentPassword(), @@ -82,22 +82,4 @@ public class AuthController { ); return ApiResponse.success(); } - - @PostMapping("/limited-accounts/{username}/change-password") - @Operation(summary = "修改当前 LIMITED 账户口令", description = "基于当前 LIMITED 会话校验并更新指定受限账户口令。") - @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前 LIMITED 账户口令") - @ReplayProtected - public ApiResponse changeLimitedAccountPassword( - @PathVariable("username") String username, - @Valid @RequestBody ChangePasswordRequest request, - HttpServletRequest httpRequest - ) { - authService.changeLimitedAccountPassword( - (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), - username, - request.getCurrentPassword(), - request.getNewPassword() - ); - return ApiResponse.success(); - } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java b/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java index 23e4549..1008b77 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/AuthSessionPrincipal.java @@ -13,9 +13,16 @@ public class AuthSessionPrincipal { return principal; } - public static AuthSessionPrincipal limited(String username) { + public static AuthSessionPrincipal full(Integer uid, String username) { + AuthSessionPrincipal principal = full(uid); + principal.setUsername(username); + return principal; + } + + public static AuthSessionPrincipal limited(Integer uid, String username) { AuthSessionPrincipal principal = new AuthSessionPrincipal(); principal.setType("LIMITED"); + principal.setUid(uid); principal.setUsername(username); return principal; } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginAccountRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginAccountRequest.java index e617057..f5ae4f6 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginAccountRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginAccountRequest.java @@ -2,24 +2,25 @@ package com.cisd.tms.modules.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; @Schema(description = "角色口令校验账号") public class PasswordLoginAccountRequest { - @NotBlank(message = "username is required") - @Schema(description = "账号名", example = "super-admin-01") - private String username; + @NotNull(message = "uid is required") + @Schema(description = "角色固定席位编号", example = "1") + private Integer uid; @NotBlank(message = "password is required") @Schema(description = "账号口令", example = "12345678") private String password; - public String getUsername() { - return username; + public Integer getUid() { + return uid; } - public void setUsername(String username) { - this.username = username; + public void setUid(Integer uid) { + this.uid = uid; } public String getPassword() { diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserAccountEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserAccountEntity.java deleted file mode 100644 index 139daeb..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserAccountEntity.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.cisd.tms.modules.auth.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; -import java.time.LocalDateTime; - -@TableName("tms_auth_user_account") -public class AuthUserAccountEntity extends BaseEntity { - - private String username; - private String displayName; - private String roleCode; - private String passwordHash; - private String passwordSalt; - private String status; - private Boolean needChangePassword; - private LocalDateTime passwordChangedAt; - private Integer failedCount; - private LocalDateTime lockedUntil; - private LocalDateTime lastLoginAt; - private LocalDateTime lastActiveAt; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getRoleCode() { - return roleCode; - } - - public void setRoleCode(String roleCode) { - this.roleCode = roleCode; - } - - public String getPasswordHash() { - return passwordHash; - } - - public void setPasswordHash(String passwordHash) { - this.passwordHash = passwordHash; - } - - public String getPasswordSalt() { - return passwordSalt; - } - - public void setPasswordSalt(String passwordSalt) { - this.passwordSalt = passwordSalt; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public Boolean getNeedChangePassword() { - return needChangePassword; - } - - public void setNeedChangePassword(Boolean needChangePassword) { - this.needChangePassword = needChangePassword; - } - - public LocalDateTime getPasswordChangedAt() { - return passwordChangedAt; - } - - public void setPasswordChangedAt(LocalDateTime passwordChangedAt) { - this.passwordChangedAt = passwordChangedAt; - } - - public Integer getFailedCount() { - return failedCount; - } - - public void setFailedCount(Integer failedCount) { - this.failedCount = failedCount; - } - - public LocalDateTime getLockedUntil() { - return lockedUntil; - } - - public void setLockedUntil(LocalDateTime lockedUntil) { - this.lockedUntil = lockedUntil; - } - - public LocalDateTime getLastLoginAt() { - return lastLoginAt; - } - - public void setLastLoginAt(LocalDateTime lastLoginAt) { - this.lastLoginAt = lastLoginAt; - } - - public LocalDateTime getLastActiveAt() { - return lastActiveAt; - } - - public void setLastActiveAt(LocalDateTime lastActiveAt) { - this.lastActiveAt = lastActiveAt; - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserEntity.java deleted file mode 100644 index 1affcd4..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/entity/AuthUserEntity.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.cisd.tms.modules.auth.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; - -@TableName("tms_auth_user") -public class AuthUserEntity extends BaseEntity { - - private String username; - private String displayName; - private String role; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java b/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java index c83cd80..b069e25 100644 --- a/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java +++ b/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java @@ -1,7 +1,7 @@ package com.cisd.tms.modules.auth.enums; public enum RoleCode { - SUPER_ADMIN("SUPER_ADMIN", "超级管理员", 2), + SUPER_ADMIN("SUPER_ADMIN", "系统管理员", 2), KEY_ADMIN("KEY_ADMIN", "密钥管理员", 1), AUDIT_ADMIN("AUDIT_ADMIN", "审计管理员", 1), OPS_ADMIN("OPS_ADMIN", "运维管理员", 1); diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserAccountMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserAccountMapper.java deleted file mode 100644 index e5c218c..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserAccountMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.cisd.tms.modules.auth.mapper; - -import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; -import java.util.List; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -@Mapper -public interface AuthUserAccountMapper extends BaseMapperX { - - AuthUserAccountEntity selectByUsername(@Param("username") String username); - - List selectByRoleCode(@Param("roleCode") String roleCode); -} diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserMapper.java deleted file mode 100644 index de266ec..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthUserMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.cisd.tms.modules.auth.mapper; - -import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; -import com.cisd.tms.modules.auth.entity.AuthUserEntity; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -@Mapper -public interface AuthUserMapper extends BaseMapperX { - - AuthUserEntity selectByUsername(@Param("username") String username); -} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserAccountRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserAccountRepository.java deleted file mode 100644 index c3f3190..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserAccountRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.cisd.tms.modules.auth.repository; - -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; -import java.util.List; -import java.util.Optional; - -public interface AuthUserAccountRepository { - - Optional findByUsername(String username); - - List findByRoleCode(String roleCode); - - void save(AuthUserAccountEntity entity); - - void update(AuthUserAccountEntity entity); -} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserRepository.java deleted file mode 100644 index ae4ca8b..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/repository/AuthUserRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.cisd.tms.modules.auth.repository; - -import com.cisd.tms.modules.auth.entity.AuthUserEntity; -import java.util.Optional; - -public interface AuthUserRepository { - - Optional findByUsername(String username); -} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserAccountRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserAccountRepositoryImpl.java deleted file mode 100644 index d7ef0a0..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserAccountRepositoryImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.cisd.tms.modules.auth.repository.impl; - -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; -import com.cisd.tms.modules.auth.mapper.AuthUserAccountMapper; -import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; -import java.util.List; -import java.util.Optional; -import org.springframework.stereotype.Repository; - -@Repository -public class AuthUserAccountRepositoryImpl implements AuthUserAccountRepository { - - private final AuthUserAccountMapper authUserAccountMapper; - - public AuthUserAccountRepositoryImpl(AuthUserAccountMapper authUserAccountMapper) { - this.authUserAccountMapper = authUserAccountMapper; - } - - @Override - public Optional findByUsername(String username) { - return Optional.ofNullable(authUserAccountMapper.selectByUsername(username)); - } - - @Override - public List findByRoleCode(String roleCode) { - return authUserAccountMapper.selectByRoleCode(roleCode); - } - - @Override - public void save(AuthUserAccountEntity entity) { - authUserAccountMapper.insert(entity); - } - - @Override - public void update(AuthUserAccountEntity entity) { - authUserAccountMapper.updateById(entity); - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserRepositoryImpl.java deleted file mode 100644 index b7645b0..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthUserRepositoryImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.cisd.tms.modules.auth.repository.impl; - -import com.cisd.tms.modules.auth.entity.AuthUserEntity; -import com.cisd.tms.modules.auth.mapper.AuthUserMapper; -import com.cisd.tms.modules.auth.repository.AuthUserRepository; -import java.util.Optional; -import org.springframework.stereotype.Repository; - -@Repository -public class AuthUserRepositoryImpl implements AuthUserRepository { - - private final AuthUserMapper authUserMapper; - - public AuthUserRepositoryImpl(AuthUserMapper authUserMapper) { - this.authUserMapper = authUserMapper; - } - - @Override - public Optional findByUsername(String username) { - return Optional.ofNullable(authUserMapper.selectByUsername(username)); - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java index afb6fd4..0b3bc7e 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java @@ -9,7 +9,7 @@ public interface AuthAdminService { void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode); - void changeFullAccountPassword( + void changeAccountPassword( String operatorRoleCode, String operatorAuthLevel, String targetRoleCode, @@ -18,15 +18,6 @@ public interface AuthAdminService { String newPassword ); - void changeLimitedAccountPassword( - String operatorRoleCode, - String operatorAuthLevel, - String targetRoleCode, - String username, - String oldPassword, - String newPassword - ); - UKeySignResult issueUkeyBindingSign( String operatorRoleCode, String operatorAuthLevel, diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java index 81dfaab..8ced75a 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java @@ -27,7 +27,5 @@ public interface AuthService { void logout(String sessionToken); - void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword); - - void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword); + void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword); } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java index 52ae1da..c312c51 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java @@ -4,13 +4,11 @@ import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; import com.cisd.tms.common.util.TraceIdUtil; import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.enums.RoleAccountStatus; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; -import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.service.AuthAdminService; @@ -40,7 +38,6 @@ public class AuthAdminServiceImpl implements AuthAdminService { private final RoleAccountRepository roleAccountRepository; private final AuthFullAccountRepository authFullAccountRepository; - private final AuthUserAccountRepository authUserAccountRepository; private final RoleUkeyBindingRepository roleUkeyBindingRepository; private final PasswordHasher passwordHasher; private final LmkService lmkService; @@ -68,19 +65,6 @@ public class AuthAdminServiceImpl implements AuthAdminService { authFullAccountRepository.update(account); } - List accounts = authUserAccountRepository.findByRoleCode(targetRoleCode); - if (accounts.isEmpty()) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found"); - } - for (AuthUserAccountEntity account : accounts) { - account.setStatus(RoleAccountStatus.ACTIVE.name()); - account.setNeedChangePassword(Boolean.TRUE); - account.setFailedCount(0); - account.setLockedUntil(null); - account.setLastActiveAt(null); - account.setLastLoginAt(null); - authUserAccountRepository.update(account); - } } @Override @@ -108,27 +92,10 @@ public class AuthAdminServiceImpl implements AuthAdminService { authFullAccountRepository.update(account); } - List accounts = authUserAccountRepository.findByRoleCode(targetRoleCode); - if (accounts.isEmpty()) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user accounts not found"); - } - for (AuthUserAccountEntity account : accounts) { - String newSalt = passwordSaltGenerator.nextSalt(); - account.setPasswordSalt(newSalt); - account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt)); - account.setStatus(RoleAccountStatus.ACTIVE.name()); - account.setNeedChangePassword(Boolean.TRUE); - account.setPasswordChangedAt(current); - account.setFailedCount(0); - account.setLockedUntil(null); - account.setLastActiveAt(null); - account.setLastLoginAt(null); - authUserAccountRepository.update(account); - } } @Override - public void changeFullAccountPassword( + public void changeAccountPassword( String operatorRoleCode, String operatorAuthLevel, String targetRoleCode, @@ -151,29 +118,6 @@ public class AuthAdminServiceImpl implements AuthAdminService { authFullAccountRepository.update(account); } - @Override - public void changeLimitedAccountPassword( - String operatorRoleCode, - String operatorAuthLevel, - String targetRoleCode, - String username, - String oldPassword, - String newPassword - ) { - PasswordComplexityValidator.validate(newPassword); - loadRole(targetRoleCode); - AuthUserAccountEntity account = authUserAccountRepository.findByUsername(username) - .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found")); - if (!targetRoleCode.equals(account.getRoleCode())) { - throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found"); - } - if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordHash())) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "old password is incorrect"); - } - applyAdminPasswordChange(account, newPassword); - authUserAccountRepository.update(account); - } - private void bindUkey( String targetRoleCode, Integer uid, @@ -277,19 +221,6 @@ public class AuthAdminServiceImpl implements AuthAdminService { } } - private void applyAdminPasswordChange(AuthUserAccountEntity account, String newPassword) { - String newSalt = passwordSaltGenerator.nextSalt(); - account.setPasswordSalt(newSalt); - account.setPasswordHash(passwordHasher.hash(newPassword, newSalt)); - account.setNeedChangePassword(Boolean.TRUE); - account.setPasswordChangedAt(now()); - account.setFailedCount(0); - account.setLockedUntil(null); - if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) { - account.setStatus(RoleAccountStatus.ACTIVE.name()); - } - } - private RoleAccountEntity loadRole(String roleCode) { resolveRoleCode(roleCode); return roleAccountRepository.findByRoleCode(roleCode) diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java index 4f97318..0675e21 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java @@ -16,8 +16,6 @@ import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; import com.cisd.tms.modules.auth.dto.UkeyLoginRequest; import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; -import com.cisd.tms.modules.auth.entity.AuthUserEntity; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.enums.AuthLevel; @@ -26,8 +24,6 @@ import com.cisd.tms.modules.auth.enums.RoleAccountStatus; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; import com.cisd.tms.modules.auth.repository.AuthSessionRepository; -import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; -import com.cisd.tms.modules.auth.repository.AuthUserRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.service.AuthPolicyService; @@ -69,8 +65,6 @@ public class AuthServiceImpl implements AuthService { private static final int PASSWORD_EXPIRE_DAYS = 30; private final RoleAccountRepository roleAccountRepository; - private final AuthUserRepository authUserRepository; - private final AuthUserAccountRepository authUserAccountRepository; private final AuthFullAccountRepository authFullAccountRepository; private final AuthSessionRepository authSessionRepository; private final RoleUkeyBindingRepository roleUkeyBindingRepository; @@ -92,19 +86,17 @@ public class AuthServiceImpl implements AuthService { validateRoleStatus(roleAccount); AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials()); - List validatedAccounts = new ArrayList<>(); - List validatedFullAccounts = new ArrayList<>(); + List validatedAccounts; if (AuthMethod.PASSWORD == authMethod) { validatedAccounts = validateRoleAccounts(roleAccount, request.getAccounts()); - resetValidatedAccounts(validatedAccounts); } else { - validatedFullAccounts = validateFullAccounts(roleAccount, request.getFullAccounts()); - resetValidatedFullAccounts(validatedFullAccounts); + validatedAccounts = validateFullAccounts(roleAccount, request.getFullAccounts()); } + resetValidatedAccounts(validatedAccounts); AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials()); List authenticatedPrincipals = AuthMethod.PASSWORD == authMethod - ? normalizeAuthenticatedUsers(validatedAccounts) - : normalizeAuthenticatedFullAccounts(validatedFullAccounts); + ? normalizeAuthenticatedAccounts(validatedAccounts, AuthLevel.LIMITED) + : normalizeAuthenticatedAccounts(validatedAccounts, AuthLevel.FULL); expireConcurrentSessions(roleAccount.getRoleCode(), authLevel, authenticatedPrincipals); AuthSessionEntity session = buildSession( roleAccount.getRoleCode(), @@ -120,9 +112,7 @@ public class AuthServiceImpl implements AuthService { response.setToken(session.getSessionToken()); response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString()); response.setNeedChangePassword( - AuthMethod.PASSWORD == authMethod - ? needsPasswordChangeForLimitedAccounts(validatedAccounts) - : needsPasswordChangeForFullAccounts(validatedFullAccounts) + needsPasswordChangeForAccounts(validatedAccounts) ); return response; } @@ -189,36 +179,19 @@ public class AuthServiceImpl implements AuthService { @Override public CurrentUserResponse me(String username, String sessionToken) { String normalized = username == null || username.isBlank() ? "admin" : username; - RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized).orElse(null); - if (roleAccount != null) { - CurrentUserResponse response = new CurrentUserResponse(); - response.setUsername(roleAccount.getRoleCode()); - response.setDisplayName(roleAccount.getDisplayName()); - response.setRole(roleAccount.getRoleCode()); - AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null); - if (session != null) { - response.setAuthLevel(session.getAuthLevel()); - response.setNeedChangePassword(needsPasswordChange(session)); - } else { - response.setNeedChangePassword(Boolean.FALSE); - } - return response; - } - - AuthUserEntity entity = authUserRepository.findByUsername(normalized).orElse(null); + RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found")); CurrentUserResponse response = new CurrentUserResponse(); - if (entity == null) { - response.setUsername(normalized); - response.setDisplayName("TMS Administrator"); - response.setRole("ADMIN"); + response.setUsername(roleAccount.getRoleCode()); + response.setDisplayName(roleAccount.getDisplayName()); + response.setRole(roleAccount.getRoleCode()); + AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null); + if (session != null) { + response.setAuthLevel(session.getAuthLevel()); + response.setNeedChangePassword(needsPasswordChange(session)); + } else { response.setNeedChangePassword(Boolean.FALSE); - return response; } - - response.setUsername(entity.getUsername()); - response.setDisplayName(entity.getDisplayName()); - response.setRole(entity.getRole()); - response.setNeedChangePassword(Boolean.FALSE); return response; } @@ -231,17 +204,14 @@ public class AuthServiceImpl implements AuthService { } @Override - public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) { + public void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) { PasswordComplexityValidator.validate(newPassword); AuthSessionEntity session = requireActiveSession(sessionToken); - if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required"); - } - if (uid == null || !containsAuthenticatedFullUid(session, uid)) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not authenticated in current session"); + if (uid == null || !containsAuthenticatedUid(session, uid)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account is not authenticated in current session"); } AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid) - .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account not found")); + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found")); validateFullAccountStatus(fullAccount); if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) { onFullPasswordFailed(fullAccount); @@ -265,49 +235,13 @@ public class AuthServiceImpl implements AuthService { authSessionRepository.update(session); } - @Override - public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) { - PasswordComplexityValidator.validate(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.setPasswordChangedAt(current); - userAccount.setLastLoginAt(current); - userAccount.setLastActiveAt(current); - authUserAccountRepository.update(userAccount); - - session.setLastActiveAt(current); - session.setExpiresAt(current.plusMinutes(IDLE_TIMEOUT_MINUTES)); - authSessionRepository.update(session); - } - private void validateRoleStatus(RoleAccountEntity roleAccount) { if (!RoleAccountStatus.ACTIVE.name().equals(roleAccount.getStatus())) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled"); } } - private List validateRoleAccounts( + private List validateRoleAccounts( RoleAccountEntity roleAccount, List accounts ) { @@ -319,54 +253,25 @@ public class AuthServiceImpl implements AuthService { ); } - Set uniqueUsernames = new LinkedHashSet<>(); - List validatedAccounts = new ArrayList<>(); + Set uniqueUids = new LinkedHashSet<>(); + List validatedAccounts = new ArrayList<>(); for (PasswordLoginAccountRequest account : accounts) { - if (!uniqueUsernames.add(account.getUsername())) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate account username submitted"); + Integer uid = account.getUid(); + if (uid == null || !uniqueUids.add(uid)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "duplicate account uid submitted"); } - AuthUserAccountEntity userAccount = authUserAccountRepository.findByUsername(account.getUsername()) - .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account not found")); - validateUserAccountRole(roleAccount.getRoleCode(), userAccount); - validateUserAccountStatus(userAccount); - if (!passwordHasher.matches(account.getPassword(), userAccount.getPasswordSalt(), userAccount.getPasswordHash())) { - onPasswordFailed(userAccount); + AuthFullAccountEntity roleAccountSeat = authFullAccountRepository + .findByRoleCodeAndUid(roleAccount.getRoleCode(), uid) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found")); + validateFullAccountStatus(roleAccountSeat); + if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordHash())) { + onFullPasswordFailed(roleAccountSeat); } - validatedAccounts.add(userAccount); + validatedAccounts.add(roleAccountSeat); } return validatedAccounts; } - private void validateUserAccountRole(String expectedRoleCode, AuthUserAccountEntity userAccount) { - if (!expectedRoleCode.equals(userAccount.getRoleCode())) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account does not belong to target role"); - } - } - - private void validateUserAccountStatus(AuthUserAccountEntity userAccount) { - if (RoleAccountStatus.UNENABLED.name().equals(userAccount.getStatus())) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is not enabled"); - } - if (RoleAccountStatus.LOCKED.name().equals(userAccount.getStatus()) - && (userAccount.getLockedUntil() == null || userAccount.getLockedUntil().isAfter(now()))) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked"); - } - } - - private void onPasswordFailed(AuthUserAccountEntity userAccount) { - int failedCount = userAccount.getFailedCount() == null ? 0 : userAccount.getFailedCount(); - failedCount++; - userAccount.setFailedCount(failedCount); - if (failedCount >= MAX_FAILED_ATTEMPTS) { - userAccount.setStatus(RoleAccountStatus.LOCKED.name()); - userAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES)); - authUserAccountRepository.update(userAccount); - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "user account is locked"); - } - authUserAccountRepository.update(userAccount); - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect"); - } - private void validateFullAccountStatus(AuthFullAccountEntity fullAccount) { if (RoleAccountStatus.UNENABLED.name().equals(fullAccount.getStatus())) { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account is not enabled"); @@ -391,19 +296,7 @@ public class AuthServiceImpl implements AuthService { throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect"); } - private void resetValidatedAccounts(List validatedAccounts) { - LocalDateTime current = now(); - for (AuthUserAccountEntity account : validatedAccounts) { - account.setFailedCount(0); - account.setLockedUntil(null); - account.setStatus(RoleAccountStatus.ACTIVE.name()); - account.setLastLoginAt(current); - account.setLastActiveAt(current); - authUserAccountRepository.update(account); - } - } - - private void resetValidatedFullAccounts(List validatedAccounts) { + private void resetValidatedAccounts(List validatedAccounts) { LocalDateTime current = now(); for (AuthFullAccountEntity account : validatedAccounts) { account.setFailedCount(0); @@ -587,25 +480,18 @@ public class AuthServiceImpl implements AuthService { } } - private List normalizeAuthenticatedUsers(List validatedAccounts) { + private List normalizeAuthenticatedAccounts( + List validatedAccounts, + AuthLevel authLevel + ) { if (validatedAccounts == null || validatedAccounts.isEmpty()) { return null; } return validatedAccounts.stream() - .map(AuthUserAccountEntity::getUsername) - .sorted() - .map(AuthSessionPrincipal::limited) - .toList(); - } - - private List normalizeAuthenticatedFullAccounts(List validatedAccounts) { - if (validatedAccounts == null || validatedAccounts.isEmpty()) { - return null; - } - return validatedAccounts.stream() - .map(AuthFullAccountEntity::getUid) - .sorted() - .map(AuthSessionPrincipal::full) + .sorted(java.util.Comparator.comparing(AuthFullAccountEntity::getUid)) + .map(account -> AuthLevel.FULL == authLevel + ? AuthSessionPrincipal.full(account.getUid(), account.getAccountName()) + : AuthSessionPrincipal.limited(account.getUid(), account.getAccountName())) .toList(); } @@ -636,7 +522,11 @@ public class AuthServiceImpl implements AuthService { if (value.startsWith("FULL:")) { principals.add(AuthSessionPrincipal.full(Integer.parseInt(value.substring("FULL:".length())))); } else { - principals.add(AuthSessionPrincipal.limited(value)); + AuthFullAccountEntity account = authFullAccountRepository.findByRoleCode(session.getRoleCode()).stream() + .filter(item -> value.equals(item.getAccountName())) + .findFirst() + .orElse(null); + principals.add(AuthSessionPrincipal.limited(account == null ? null : account.getUid(), value)); } } return principals; @@ -650,25 +540,16 @@ public class AuthServiceImpl implements AuthService { } } - private boolean containsAuthenticatedFullUid(AuthSessionEntity session, Integer uid) { + private boolean containsAuthenticatedUid(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())); + .anyMatch(principal -> uid.equals(principal.getUid())); } private LocalDateTime now() { return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); } - private boolean needsPasswordChangeForLimitedAccounts(List accounts) { - return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange); - } - - private boolean needsPasswordChangeForFullAccounts(List accounts) { + private boolean needsPasswordChangeForAccounts(List accounts) { return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange); } @@ -680,18 +561,10 @@ public class AuthServiceImpl implements AuthService { if (principals.isEmpty()) { return false; } - if (AuthLevel.FULL.name().equals(session.getAuthLevel())) { - List accounts = authFullAccountRepository.findByRoleCode(session.getRoleCode()); - return accounts.stream() - .filter(account -> principals.stream() - .anyMatch(principal -> "FULL".equals(principal.getType()) && account.getUid().equals(principal.getUid()))) - .anyMatch(this::needsPasswordChange); - } - List accounts = authUserAccountRepository.findByRoleCode(session.getRoleCode()); + List accounts = authFullAccountRepository.findByRoleCode(session.getRoleCode()); return accounts.stream() .filter(account -> principals.stream() - .anyMatch(principal -> "LIMITED".equals(principal.getType()) - && account.getUsername().equals(principal.getUsername()))) + .anyMatch(principal -> account.getUid().equals(principal.getUid()))) .anyMatch(this::needsPasswordChange); } @@ -700,11 +573,6 @@ public class AuthServiceImpl implements AuthService { || isPasswordExpired(account.getPasswordChangedAt()); } - private boolean needsPasswordChange(AuthUserAccountEntity account) { - return Boolean.TRUE.equals(account.getNeedChangePassword()) - || isPasswordExpired(account.getPasswordChangedAt()); - } - private boolean isPasswordExpired(LocalDateTime passwordChangedAt) { return passwordChangedAt == null || !passwordChangedAt.isAfter(now().minusDays(PASSWORD_EXPIRE_DAYS)); } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java index e52fc62..20fd7a0 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifier.java @@ -37,6 +37,9 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { private static final String PROVIDER = "BC"; private static final String SIGNATURE_ALGORITHM = "SM3withSM2"; private static final String SM2_CURVE = "sm2p256v1"; + private static final int GM0018_ECCREF_PUBLIC_KEY_LENGTH = 132; + private static final int GM0018_ECCREF_COMPONENT_LENGTH = 64; + private static final int SM2_COORDINATE_LENGTH = 32; static { if (Security.getProvider(PROVIDER) == null) { @@ -65,6 +68,9 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { public void verifyLoginSignature(String pubKey, String loginSignData, String loginSign) { try { PublicKey publicKey = parsePublicKey(pubKey); + if (loginSignData == null || loginSignData.isBlank()) { + throw new IllegalArgumentException("login sign data is required"); + } byte[] signatureBytes = decodeSignature(loginSign); Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER); signature.initVerify(publicKey); @@ -96,7 +102,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { try { return KeyFactory.getInstance("EC", PROVIDER).generatePublic(new X509EncodedKeySpec(decoded)); } catch (Exception ignored) { - return parseUncompressedPointPublicKey(decoded); + return parseUncompressedPointPublicKey(toPointBytes(decoded)); } } @@ -110,12 +116,40 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { } } - private PublicKey parseUncompressedPointPublicKey(byte[] keyBytes) throws Exception { - byte[] pointBytes = keyBytes; + private byte[] toPointBytes(byte[] keyBytes) { + if (keyBytes.length == GM0018_ECCREF_PUBLIC_KEY_LENGTH) { + return gm0018EccRefToPointBytes(keyBytes); + } + return keyBytes; + } + + private byte[] gm0018EccRefToPointBytes(byte[] keyBytes) { + int bitsLittleEndian = (keyBytes[0] & 0xff) + | ((keyBytes[1] & 0xff) << 8) + | ((keyBytes[2] & 0xff) << 16) + | ((keyBytes[3] & 0xff) << 24); + int bitsBigEndian = ((keyBytes[0] & 0xff) << 24) + | ((keyBytes[1] & 0xff) << 16) + | ((keyBytes[2] & 0xff) << 8) + | (keyBytes[3] & 0xff); + if (bitsLittleEndian != 256 && bitsBigEndian != 256) { + throw new IllegalArgumentException("login public key is invalid"); + } + byte[] pointBytes = new byte[65]; + pointBytes[0] = 0x04; + System.arraycopy(keyBytes, 4 + GM0018_ECCREF_COMPONENT_LENGTH - SM2_COORDINATE_LENGTH, + pointBytes, 1, SM2_COORDINATE_LENGTH); + System.arraycopy(keyBytes, 4 + GM0018_ECCREF_COMPONENT_LENGTH * 2 - SM2_COORDINATE_LENGTH, + pointBytes, 1 + SM2_COORDINATE_LENGTH, SM2_COORDINATE_LENGTH); + return pointBytes; + } + + private PublicKey parseUncompressedPointPublicKey(byte[] pointBytes) throws Exception { if (pointBytes.length == 64) { + byte[] rawPointBytes = pointBytes; pointBytes = new byte[65]; pointBytes[0] = 0x04; - System.arraycopy(keyBytes, 0, pointBytes, 1, keyBytes.length); + System.arraycopy(rawPointBytes, 0, pointBytes, 1, rawPointBytes.length); } if (pointBytes.length != 65 || pointBytes[0] != 0x04) { throw new IllegalArgumentException("login public key is invalid"); @@ -129,16 +163,31 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { } private byte[] decodeSignature(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("login signature is required"); + } byte[] decoded = decodeHexOrBase64(normalize(value), "login signature is invalid"); if (isDerSignature(decoded)) { return decoded; } + if (decoded.length == GM0018_ECCREF_COMPONENT_LENGTH * 2) { + return rawRsToDer(gm0018SignatureToRawRs(decoded)); + } if (decoded.length == 64) { return rawRsToDer(decoded); } throw new IllegalArgumentException("login signature is invalid"); } + private byte[] gm0018SignatureToRawRs(byte[] signatureBytes) { + byte[] rawSignature = new byte[64]; + System.arraycopy(signatureBytes, GM0018_ECCREF_COMPONENT_LENGTH - SM2_COORDINATE_LENGTH, + rawSignature, 0, SM2_COORDINATE_LENGTH); + System.arraycopy(signatureBytes, GM0018_ECCREF_COMPONENT_LENGTH * 2 - SM2_COORDINATE_LENGTH, + rawSignature, SM2_COORDINATE_LENGTH, SM2_COORDINATE_LENGTH); + return rawSignature; + } + private boolean isDerSignature(byte[] signatureBytes) { try (ASN1InputStream inputStream = new ASN1InputStream(signatureBytes)) { ASN1Primitive primitive = inputStream.readObject(); diff --git a/src/main/java/com/cisd/tms/modules/log/aspect/OperationAuditAspect.java b/src/main/java/com/cisd/tms/modules/log/aspect/OperationAuditAspect.java index 7a2e9de..5dad2e5 100644 --- a/src/main/java/com/cisd/tms/modules/log/aspect/OperationAuditAspect.java +++ b/src/main/java/com/cisd/tms/modules/log/aspect/OperationAuditAspect.java @@ -142,7 +142,7 @@ public class OperationAuditAspect { return ""; } return request.getAccounts().stream() - .map(account -> account.getUsername() == null ? "" : account.getUsername().trim()) + .map(account -> account.getUid() == null ? "" : "uid=" + account.getUid()) .filter(value -> !value.isBlank()) .collect(Collectors.joining(",")); } diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index 12b8214..f1ab45d 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -139,16 +139,6 @@ CREATE TABLE IF NOT EXISTS tms_resource_task_step ( -- 认证与设备基础表 -- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS tms_auth_user ( - id BIGINT PRIMARY KEY, - username VARCHAR(64) NOT NULL, - display_name VARCHAR(128) NOT NULL, - role VARCHAR(64) NOT NULL, - create_time DATETIME(3) NOT NULL, - update_time DATETIME(3) NOT NULL, - UNIQUE KEY uk_tms_auth_user_username (username) -); - CREATE TABLE IF NOT EXISTS tms_device_node ( id BIGINT PRIMARY KEY, node_id VARCHAR(64) NOT NULL, @@ -267,26 +257,6 @@ CREATE TABLE IF NOT EXISTS tms_role_ukey_binding ( KEY idx_tms_role_ukey_binding_role_status (role_code, status) ); -CREATE TABLE IF NOT EXISTS tms_auth_user_account ( - id BIGINT PRIMARY KEY, - username VARCHAR(64) NOT NULL, - display_name VARCHAR(128) NOT NULL, - role_code VARCHAR(64) NOT NULL, - password_hash VARCHAR(256) NOT NULL, - password_salt VARCHAR(128) NOT NULL, - status VARCHAR(32) NOT NULL, - need_change_password TINYINT(1) NOT NULL DEFAULT 1, - password_changed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - failed_count INT NOT NULL DEFAULT 0, - locked_until DATETIME(3) NULL, - last_login_at DATETIME(3) NULL, - last_active_at DATETIME(3) NULL, - create_time DATETIME(3) NOT NULL, - update_time DATETIME(3) NOT NULL, - UNIQUE KEY uk_tms_auth_user_account_username (username), - KEY idx_tms_auth_user_account_role_code (role_code) -); - CREATE TABLE IF NOT EXISTS tms_auth_full_account ( id BIGINT PRIMARY KEY, role_code VARCHAR(64) NOT NULL, @@ -439,110 +409,6 @@ VALUES CURRENT_TIMESTAMP(3) ); -INSERT IGNORE INTO tms_auth_user_account ( - id, - username, - display_name, - role_code, - password_hash, - password_salt, - status, - need_change_password, - password_changed_at, - failed_count, - locked_until, - last_login_at, - last_active_at, - create_time, - update_time -) -VALUES - ( - 12001, - 'super-admin-01', - '超级管理员账号一', - 'SUPER_ADMIN', - 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', - 'init-super-admin-salt-20260325', - 'ACTIVE', - 1, - CURRENT_TIMESTAMP(3), - 0, - NULL, - NULL, - NULL, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) - ), - ( - 12002, - 'super-admin-02', - '超级管理员账号二', - 'SUPER_ADMIN', - 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', - 'init-super-admin-salt-20260325', - 'ACTIVE', - 1, - CURRENT_TIMESTAMP(3), - 0, - NULL, - NULL, - NULL, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) - ), - ( - 12003, - 'key-admin-01', - '密钥管理员账号', - 'KEY_ADMIN', - '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', - 'init-key-admin-salt-20260325', - 'ACTIVE', - 1, - CURRENT_TIMESTAMP(3), - 0, - NULL, - NULL, - NULL, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) - ), - ( - 12004, - 'audit-admin-01', - '审计管理员账号', - 'AUDIT_ADMIN', - 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', - 'init-audit-admin-salt-20260325', - 'ACTIVE', - 1, - CURRENT_TIMESTAMP(3), - 0, - NULL, - NULL, - NULL, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) - ), - ( - 12005, - 'ops-admin-01', - '运维管理员账号', - 'OPS_ADMIN', - '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', - 'init-ops-admin-salt-20260325', - 'ACTIVE', - 1, - CURRENT_TIMESTAMP(3), - 0, - NULL, - NULL, - NULL, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) - ); - INSERT IGNORE INTO tms_auth_full_account ( id, role_code, diff --git a/src/main/resources/mapper/auth/AuthUserAccountMapper.xml b/src/main/resources/mapper/auth/AuthUserAccountMapper.xml deleted file mode 100644 index 9d505d9..0000000 --- a/src/main/resources/mapper/auth/AuthUserAccountMapper.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/mapper/auth/AuthUserMapper.xml b/src/main/resources/mapper/auth/AuthUserMapper.xml deleted file mode 100644 index b57d9dc..0000000 --- a/src/main/resources/mapper/auth/AuthUserMapper.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java index 58f5028..d71b77a 100644 --- a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java @@ -48,7 +48,7 @@ class AuthControllerTest { "roleCode": "AUDIT_ADMIN", "accounts": [ { - "username": "audit-admin-01", + "uid": 1, "password": "12345678" } ], @@ -199,7 +199,7 @@ class AuthControllerTest { } @Test - void shouldChangeFullAccountPasswordThroughCurrentSessionEndpoint() throws Exception { + void shouldChangeAccountPasswordThroughCurrentSessionEndpoint() throws Exception { AuthService authService = Mockito.mock(AuthService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); @@ -208,7 +208,7 @@ class AuthControllerTest { .setControllerAdvice(new GlobalExceptionHandler()) .build(); - mockMvc.perform(post("/api/v1/auth/full-accounts/1/change-password") + mockMvc.perform(post("/api/v1/auth/accounts/1/change-password") .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001") .contentType(MediaType.APPLICATION_JSON) .content(""" @@ -220,32 +220,7 @@ class AuthControllerTest { .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); - } - - @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(""" - { - "oldPassword": "12345678", - "newPassword": "Abc1234!" - } - """)) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("\"success\":true"))); - - Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "Abc1234!"); + Mockito.verify(authService).changeAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); } @Test @@ -268,7 +243,7 @@ class AuthControllerTest { } @Test - void shouldChangeFullAccountPasswordThroughAdminEndpoint() throws Exception { + void shouldChangeAccountPasswordThroughAdminEndpoint() throws Exception { AuthService authService = Mockito.mock(AuthService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); @@ -277,7 +252,7 @@ class AuthControllerTest { .setControllerAdvice(new GlobalExceptionHandler()) .build(); - mockMvc.perform(post("/api/v1/auth/roles/KEY_ADMIN/full-accounts/1/change-password") + mockMvc.perform(post("/api/v1/auth/roles/KEY_ADMIN/accounts/1/change-password") .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN") .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") .contentType(MediaType.APPLICATION_JSON) @@ -290,33 +265,7 @@ class AuthControllerTest { .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!"); - } - - @Test - void shouldChangeLimitedAccountPasswordThroughAdminEndpoint() 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/roles/AUDIT_ADMIN/limited-accounts/audit-admin-01/change-password") - .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN") - .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "oldPassword": "12345678", - "newPassword": "Abc1234!" - } - """)) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("\"success\":true"))); - - Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "Abc1234!"); + Mockito.verify(authAdminService).changeAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!"); } @Test diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java index b02756b..87f5b8a 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java @@ -1,6 +1,7 @@ package com.cisd.tms.modules.auth.service; -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; @@ -8,21 +9,20 @@ import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.RoleAccountStatus; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; -import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.service.impl.AuthAdminServiceImpl; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.UKeySignDTO; import com.cisd.tms.modules.mk.dto.UKeySignResult; import com.cisd.tms.modules.mk.service.LmkService; +import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; -import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Optional; @@ -33,412 +33,148 @@ import org.mockito.Mockito; class AuthAdminServiceTest { - private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T03:00:00Z"), ZoneOffset.UTC); + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); @Test - void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED)); - fullAccounts.save(fullAccount(RoleCode.AUDIT_ADMIN, 1, "audit-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1")); - userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "OLD-HASH-1", "OLD-SALT-1")); - fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name()); - userAccounts.findByUsername("audit-admin-01").orElseThrow().setStatus(RoleAccountStatus.UNENABLED.name()); + void shouldEnableRoleAndUnifiedSeatAccounts() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + RoleAccountEntity role = role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED); + roles.save(role); + AuthFullAccountEntity account = account(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "HASH-OLD", "SALT-OLD"); + account.setStatus(RoleAccountStatus.UNENABLED.name()); + account.setFailedCount(3); + account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 0)); + accounts.save(account); - AuthAdminService service = newAuthAdminService( - roleAccounts, - fullAccounts, - userAccounts, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-001") - ); + service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) + .enableRole(RoleCode.SUPER_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(); - AuthFullAccountEntity fullAccount = fullAccounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow(); - AuthUserAccountEntity userAccount = userAccounts.findByUsername("audit-admin-01").orElseThrow(); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullAccount.getStatus()); - Assertions.assertTrue(Boolean.TRUE.equals(fullAccount.getNeedChangePassword())); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), userAccount.getStatus()); - Assertions.assertTrue(Boolean.TRUE.equals(userAccount.getNeedChangePassword())); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), roles.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow().getStatus()); + AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.AUDIT_ADMIN.getCode(), 1).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); + Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); + Assertions.assertEquals(0, changed.getFailedCount()); + Assertions.assertNull(changed.getLockedUntil()); } @Test - void shouldResetPasswordForAllAccountsUnderRole() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); - fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-full-01", "OLD-FULL-HASH-1", "OLD-FULL-SALT-1")); - fullAccounts.save(fullAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-full-02", "OLD-FULL-HASH-2", "OLD-FULL-SALT-2")); - userAccounts.save(user("super-admin-01", RoleCode.SUPER_ADMIN, "OLD-HASH-1", "OLD-SALT-1")); - userAccounts.save(user("super-admin-02", RoleCode.SUPER_ADMIN, "OLD-HASH-2", "OLD-SALT-2")); - fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setFailedCount(2); - fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 20)); - fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setFailedCount(5); - fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 25)); - userAccounts.findByUsername("super-admin-01").orElseThrow().setFailedCount(3); - userAccounts.findByUsername("super-admin-01").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30)); - userAccounts.findByUsername("super-admin-02").orElseThrow().setFailedCount(5); - userAccounts.findByUsername("super-admin-02").orElseThrow().setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 40)); + void shouldResetOnlyUnifiedSeatPasswords() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "OLD-HASH-1", "OLD-SALT-1")); + accounts.save(account(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "OLD-HASH-2", "OLD-SALT-2")); - AuthAdminService service = newAuthAdminService( - roleAccounts, - fullAccounts, - userAccounts, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-full-001", "salt-full-002", "salt-002", "salt-003") - ); + service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) + .resetPassword(RoleCode.SUPER_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(); - AuthFullAccountEntity fullFirst = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); - AuthFullAccountEntity fullSecond = fullAccounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); - AuthUserAccountEntity first = userAccounts.findByUsername("super-admin-01").orElseThrow(); - AuthUserAccountEntity second = userAccounts.findByUsername("super-admin-02").orElseThrow(); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), role.getStatus()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullFirst.getStatus()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullSecond.getStatus()); - Assertions.assertTrue(Boolean.TRUE.equals(fullFirst.getNeedChangePassword())); - Assertions.assertTrue(Boolean.TRUE.equals(fullSecond.getNeedChangePassword())); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullFirst.getPasswordChangedAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullSecond.getPasswordChangedAt()); - Assertions.assertEquals(0, fullFirst.getFailedCount()); - Assertions.assertEquals(0, fullSecond.getFailedCount()); - Assertions.assertNull(fullFirst.getLockedUntil()); - Assertions.assertNull(fullSecond.getLockedUntil()); - Assertions.assertEquals("salt-full-001", fullFirst.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-full-001", fullFirst.getPasswordHash()); - Assertions.assertEquals("salt-full-002", fullSecond.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-full-002", fullSecond.getPasswordHash()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), first.getStatus()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus()); + AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); + AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); + Assertions.assertEquals("salt-reset-1", first.getPasswordSalt()); + Assertions.assertEquals("HASH:12345678:salt-reset-1", first.getPasswordHash()); + Assertions.assertEquals("salt-reset-2", second.getPasswordSalt()); + Assertions.assertEquals("HASH:12345678:salt-reset-2", second.getPasswordHash()); Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword())); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), first.getPasswordChangedAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), second.getPasswordChangedAt()); - Assertions.assertEquals(0, first.getFailedCount()); - Assertions.assertEquals(0, second.getFailedCount()); - Assertions.assertNull(first.getLockedUntil()); - Assertions.assertNull(second.getLockedUntil()); - Assertions.assertEquals("salt-002", first.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-002", first.getPasswordHash()); - Assertions.assertEquals("salt-003", second.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:salt-003", second.getPasswordHash()); } @Test - void shouldChangeTargetFullAccountPasswordByAdmin() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - roleAccounts.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); - fullAccounts.save(fullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "HASH:12345678:OLD-FULL-SALT", "OLD-FULL-SALT")); - AuthFullAccountEntity account = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); - account.setStatus(RoleAccountStatus.LOCKED.name()); - account.setFailedCount(5); - account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 50)); + void shouldChangeUnifiedSeatPassword() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:Old1234!:SALT-OLD", "SALT-OLD")); - AuthAdminService service = newAuthAdminService( - roleAccounts, - fullAccounts, - new InMemoryAuthUserAccountRepository(), - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-full-admin") - ); + service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) + .changeAccountPassword( + RoleCode.SUPER_ADMIN.getCode(), + AuthLevel.FULL.name(), + RoleCode.KEY_ADMIN.getCode(), + 1, + "Old1234!", + "New1234!" + ); - service.changeFullAccountPassword( - RoleCode.SUPER_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.KEY_ADMIN.getCode(), - 1, - "12345678", - "Abc1234!" - ); - - AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); - Assertions.assertEquals("salt-full-admin", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:Abc1234!:salt-full-admin", changed.getPasswordHash()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); + AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); + Assertions.assertEquals("salt-reset-1", changed.getPasswordSalt()); + Assertions.assertEquals("HASH:New1234!:salt-reset-1", changed.getPasswordHash()); Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt()); - Assertions.assertEquals(0, changed.getFailedCount()); - Assertions.assertNull(changed.getLockedUntil()); - } - - @Test - void shouldChangeTargetLimitedAccountPasswordByAdmin() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE)); - userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT")); - AuthUserAccountEntity account = userAccounts.findByUsername("audit-admin-01").orElseThrow(); - account.setStatus(RoleAccountStatus.LOCKED.name()); - account.setFailedCount(5); - account.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 55)); - - AuthAdminService service = newAuthAdminService( - roleAccounts, - new InMemoryAuthFullAccountRepository(), - userAccounts, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-limited-admin") - ); - - service.changeLimitedAccountPassword( - RoleCode.SUPER_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.AUDIT_ADMIN.getCode(), - "audit-admin-01", - "12345678", - "Abc1234!" - ); - - AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow(); - Assertions.assertEquals("salt-limited-admin", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:Abc1234!:salt-limited-admin", changed.getPasswordHash()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); - Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt()); - Assertions.assertEquals(0, changed.getFailedCount()); - Assertions.assertNull(changed.getLockedUntil()); } @Test void shouldRejectAdminPasswordChangeWhenOldPasswordIsWrong() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE)); - userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT")); + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:Old1234!:SALT-OLD", "SALT-OLD")); - AuthAdminService service = newAuthAdminService( - roleAccounts, - new InMemoryAuthFullAccountRepository(), - userAccounts, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-unused") - ); - - com.cisd.tms.common.exception.BizException exception = Assertions.assertThrows( - com.cisd.tms.common.exception.BizException.class, - () -> service.changeLimitedAccountPassword( - RoleCode.SUPER_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.AUDIT_ADMIN.getCode(), - "audit-admin-01", - "bad-password", - "Abc1234!" - ) - ); + BizException exception = Assertions.assertThrows(BizException.class, + () -> service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) + .changeAccountPassword( + RoleCode.SUPER_ADMIN.getCode(), + AuthLevel.FULL.name(), + RoleCode.KEY_ADMIN.getCode(), + 1, + "bad-password", + "New1234!" + )); + Assertions.assertEquals(ErrorCode.UNAUTHORIZED.getCode(), exception.getCode()); Assertions.assertEquals("old password is incorrect", exception.getMessage()); - AuthUserAccountEntity unchanged = userAccounts.findByUsername("audit-admin-01").orElseThrow(); - Assertions.assertEquals("OLD-SALT", unchanged.getPasswordSalt()); - Assertions.assertEquals("HASH:12345678:OLD-SALT", unchanged.getPasswordHash()); - } - - @Test - void shouldRejectAdminPasswordChangeWhenNewPasswordIsTooSimple() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE)); - userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT")); - - AuthAdminService service = newAuthAdminService( - roleAccounts, - new InMemoryAuthFullAccountRepository(), - userAccounts, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-unused") - ); - - com.cisd.tms.common.exception.BizException exception = Assertions.assertThrows( - com.cisd.tms.common.exception.BizException.class, - () -> service.changeLimitedAccountPassword( - RoleCode.SUPER_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.AUDIT_ADMIN.getCode(), - "audit-admin-01", - "12345678", - "87654321" - ) - ); - - Assertions.assertEquals("密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含", exception.getMessage()); - } - - @Test - void shouldUpdateActiveBindingWhenIssueSignUsesSameSerial() { - InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); - bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")); - LmkService lmkService = Mockito.mock(LmkService.class); - Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("SIG-NEW"); - com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket(); - packet.setPacketIndex(1); - Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); - - AuthAdminService service = new AuthAdminServiceImpl( - new InMemoryRoleAccountRepository(), - fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")), - new InMemoryAuthUserAccountRepository(), - bindings, - new FakePasswordHasher(), - lmkService, - new com.fasterxml.jackson.databind.ObjectMapper(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-001") - ); - - UKeySignDTO dto = new UKeySignDTO(); - dto.setPubKey("PUB-NEW"); - dto.setUkeySerial("UK-OLD"); - dto.setUid(1); - - service.issueUkeyBindingSign( - null, - null, - RoleCode.SUPER_ADMIN.getCode(), - dto - ); - - RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).stream() - .filter(binding -> "UK-OLD".equals(binding.getUkeySerial())) - .findFirst() - .orElseThrow(); - Assertions.assertEquals("UK-OLD", updated.getUkeySerial()); - Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey()); - Assertions.assertEquals("SIG-NEW", updated.getIssuerSign()); - } - - @Test - void shouldIssueBindingSignWithBackupPacketForSuperAdmin() { - LmkService lmkService = Mockito.mock(LmkService.class); - Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-001"); - com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket(); - packet.setPacketIndex(1); - Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); - - AuthAdminService service = new AuthAdminServiceImpl( - new InMemoryRoleAccountRepository(), - fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-001", "PUB-OLD", "SIG-OLD")), - new InMemoryAuthUserAccountRepository(), - new InMemoryRoleUkeyBindingRepository(), - new FakePasswordHasher(), - lmkService, - new com.fasterxml.jackson.databind.ObjectMapper(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-001") - ); - UKeySignDTO dto = new UKeySignDTO(); - dto.setPubKey("PUB-001"); - dto.setUkeySerial("UK-001"); - dto.setUid(1); - - UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); - - Assertions.assertEquals("ISSUE-SIGN-001", result.getSign()); - Assertions.assertNotNull(result.getBackupPacket()); - Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex()); - } - - @Test - void shouldIssueAndBindNormalRoleUkeyInOneStep() { - InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); - LmkService lmkService = Mockito.mock(LmkService.class); - Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-KEY-001"); - - AuthAdminService service = new AuthAdminServiceImpl( - new InMemoryRoleAccountRepository(), - fullAccountRepository(binding(RoleCode.KEY_ADMIN, 1, "UK-KEY-001", "PUB-OLD", "SIG-OLD")), - new InMemoryAuthUserAccountRepository(), - bindings, - new FakePasswordHasher(), - lmkService, - new com.fasterxml.jackson.databind.ObjectMapper(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-001") - ); - UKeySignDTO dto = new UKeySignDTO(); - dto.setPubKey("PUB-KEY-001"); - dto.setUkeySerial("UK-KEY-001"); - dto.setUid(1); - - UKeySignResult result = service.issueUkeyBindingSign( - RoleCode.SUPER_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.KEY_ADMIN.getCode(), - dto - ); - - Assertions.assertEquals("ISSUE-SIGN-KEY-001", result.getSign()); - Assertions.assertNull(result.getBackupPacket()); - RoleUkeyBindingEntity binding = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).stream() - .filter(item -> "UK-KEY-001".equals(item.getUkeySerial())) - .findFirst() - .orElseThrow(); - Assertions.assertEquals("PUB-KEY-001", binding.getUkeyPubkey()); - Assertions.assertEquals("ISSUE-SIGN-KEY-001", binding.getIssuerSign()); } @Test void shouldIssueSuperAdminBackupUkeyWithoutReplacingExistingBindingForSameUid() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(role(RoleCode.SUPER_ADMIN, RoleAccountStatus.ACTIVE)); + accounts.save(account(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "HASH-OLD", "SALT-OLD")); bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")); LmkService lmkService = Mockito.mock(LmkService.class); Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-BACKUP"); - com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket(); + Mockito.when(lmkService.signIk(Mockito.anyString())).thenReturn("ISSUE-SIGN-BACKUP"); + MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); packet.setPacketIndex(1); Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet); - - AuthAdminService service = new AuthAdminServiceImpl( - new InMemoryRoleAccountRepository(), - fullAccountRepository(binding(RoleCode.SUPER_ADMIN, 1, "UK-PRIMARY", "PUB-PRIMARY", "SIG-PRIMARY")), - new InMemoryAuthUserAccountRepository(), - bindings, - new FakePasswordHasher(), - lmkService, - new com.fasterxml.jackson.databind.ObjectMapper(), - FIXED_CLOCK, - new FixedSaltSupplier("salt-001") - ); UKeySignDTO dto = new UKeySignDTO(); dto.setPubKey("PUB-BACKUP"); dto.setUkeySerial("UK-BACKUP"); dto.setUid(1); - UKeySignResult result = service.issueUkeyBindingSign( - RoleCode.KEY_ADMIN.getCode(), - AuthLevel.FULL.name(), - RoleCode.SUPER_ADMIN.getCode(), - dto - ); + UKeySignResult result = service(roles, accounts, bindings, lmkService) + .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); Assertions.assertEquals("ISSUE-SIGN-BACKUP", result.getSign()); + Assertions.assertNotNull(result.getBackupPacket()); List activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); Assertions.assertEquals(2, activeUidBindings.size()); - Assertions.assertTrue(activeUidBindings.stream().anyMatch(binding -> "UK-PRIMARY".equals(binding.getUkeySerial()) - && "PUB-PRIMARY".equals(binding.getUkeyPubkey()))); - Assertions.assertTrue(activeUidBindings.stream().anyMatch(binding -> "UK-BACKUP".equals(binding.getUkeySerial()) - && "PUB-BACKUP".equals(binding.getUkeyPubkey()) - && "ISSUE-SIGN-BACKUP".equals(binding.getIssuerSign()))); + Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-PRIMARY".equals(item.getUkeySerial()))); + Assertions.assertTrue(activeUidBindings.stream().anyMatch(item -> "UK-BACKUP".equals(item.getUkeySerial()))); + } + + private static AuthAdminService service( + InMemoryRoleAccountRepository roles, + InMemoryAuthFullAccountRepository accounts, + InMemoryRoleUkeyBindingRepository bindings, + LmkService lmkService + ) { + return new AuthAdminServiceImpl( + roles, + accounts, + bindings, + new FakePasswordHasher(), + lmkService, + new ObjectMapper(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-reset-1", "salt-reset-2", "salt-reset-3") + ); } private static RoleAccountEntity role(RoleCode roleCode, RoleAccountStatus status) { RoleAccountEntity entity = new RoleAccountEntity(); - entity.setId((long) roleCode.ordinal() + 100); + entity.setId((long) roleCode.ordinal() + 1); entity.setRoleCode(roleCode.getCode()); entity.setDisplayName(roleCode.getDisplayName()); entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount()); @@ -446,22 +182,7 @@ class AuthAdminServiceTest { return entity; } - private static AuthUserAccountEntity user(String username, RoleCode roleCode, String passwordHash, String passwordSalt) { - AuthUserAccountEntity entity = new AuthUserAccountEntity(); - entity.setId((long) (username.hashCode() & Integer.MAX_VALUE)); - entity.setUsername(username); - entity.setDisplayName(username); - entity.setRoleCode(roleCode.getCode()); - entity.setPasswordHash(passwordHash); - entity.setPasswordSalt(passwordSalt); - entity.setStatus(RoleAccountStatus.ACTIVE.name()); - entity.setNeedChangePassword(Boolean.FALSE); - entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0)); - entity.setFailedCount(0); - return entity; - } - - private static AuthFullAccountEntity fullAccount(RoleCode roleCode, int uid, String accountName, String passwordHash, String passwordSalt) { + private static AuthFullAccountEntity account(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()); @@ -472,32 +193,24 @@ class AuthAdminServiceTest { entity.setPasswordSalt(passwordSalt); entity.setStatus(RoleAccountStatus.ACTIVE.name()); entity.setNeedChangePassword(Boolean.FALSE); - entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0)); + entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0)); entity.setFailedCount(0); return entity; } - private static RoleUkeyBindingEntity binding( - RoleCode roleCode, - int uid, - String serial, - String pubkey, - String issuerSign - ) { + private static RoleUkeyBindingEntity binding(RoleCode roleCode, int uid, String serial, String pubKey, String sign) { RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity(); - entity.setId((long) uid); + entity.setId((long) Math.abs((roleCode.getCode() + uid + serial).hashCode())); entity.setRoleCode(roleCode.getCode()); entity.setUid(uid); entity.setUkeySerial(serial); - entity.setUkeyPubkey(pubkey); - entity.setIssuerSign(issuerSign); + entity.setUkeyPubkey(pubKey); + entity.setIssuerSign(sign); entity.setStatus("ACTIVE"); - entity.setBoundAt(LocalDateTime.of(2026, 3, 22, 8, 0)); return entity; } private static class FakePasswordHasher implements PasswordHasher { - @Override public String hash(String password, String salt) { return "HASH:" + password + ":" + salt; @@ -510,15 +223,19 @@ class AuthAdminServiceTest { } private static class FixedSaltSupplier implements PasswordSaltGenerator { - private final Deque salts; + private final List salts; + private int index = 0; private FixedSaltSupplier(String... salts) { - this.salts = new ArrayDeque<>(List.of(salts)); + this.salts = List.of(salts); } @Override public String nextSalt() { - return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback"); + if (index >= salts.size()) { + return salts.get(salts.size() - 1); + } + return salts.get(index++); } } @@ -541,33 +258,6 @@ class AuthAdminServiceTest { } } - private static class InMemoryAuthUserAccountRepository implements AuthUserAccountRepository { - private final Map store = new ConcurrentHashMap<>(); - - @Override - public Optional findByUsername(String username) { - return Optional.ofNullable(store.get(username)); - } - - @Override - public List findByRoleCode(String roleCode) { - return store.values().stream() - .filter(entity -> roleCode.equals(entity.getRoleCode())) - .sorted(Comparator.comparing(AuthUserAccountEntity::getUsername)) - .toList(); - } - - @Override - public void save(AuthUserAccountEntity entity) { - store.put(entity.getUsername(), entity); - } - - @Override - public void update(AuthUserAccountEntity entity) { - store.put(entity.getUsername(), entity); - } - } - private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository { private final Map store = new ConcurrentHashMap<>(); @@ -591,75 +281,37 @@ class AuthAdminServiceTest { @Override public void update(AuthFullAccountEntity entity) { - store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); + save(entity); } } private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { - private final Map> store = new ConcurrentHashMap<>(); + private final List store = new ArrayList<>(); @Override - public List findActiveByRoleCodeAndUid(String roleCode, Integer uid) { - return store.getOrDefault(roleCode, List.of()).stream() - .filter(entity -> uid.equals(entity.getUid())) + public List findActiveByRoleCode(String roleCode) { + return store.stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) .filter(entity -> "ACTIVE".equals(entity.getStatus())) - .sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed()) .toList(); } @Override - public List findActiveByRoleCode(String roleCode) { - return store.getOrDefault(roleCode, List.of()).stream() - .filter(entity -> "ACTIVE".equals(entity.getStatus())) - .sorted(Comparator.comparing(RoleUkeyBindingEntity::getUid)) + public List findActiveByRoleCodeAndUid(String roleCode, Integer uid) { + return findActiveByRoleCode(roleCode).stream() + .filter(entity -> uid.equals(entity.getUid())) .toList(); } @Override public void save(RoleUkeyBindingEntity entity) { - store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()).add(entity); + store.add(entity); } @Override public void update(RoleUkeyBindingEntity entity) { - List list = store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()); - list.removeIf(item -> item.getId().equals(entity.getId())); - list.add(entity); + store.removeIf(item -> item.getId().equals(entity.getId())); + store.add(entity); } } - - private static AuthAdminService newAuthAdminService( - InMemoryRoleAccountRepository roleAccounts, - InMemoryAuthFullAccountRepository fullAccounts, - InMemoryAuthUserAccountRepository userAccounts, - InMemoryRoleUkeyBindingRepository bindings, - Clock clock, - PasswordSaltGenerator saltGenerator - ) { - return new AuthAdminServiceImpl( - roleAccounts, - fullAccounts, - userAccounts, - bindings, - new FakePasswordHasher(), - Mockito.mock(LmkService.class), - new com.fasterxml.jackson.databind.ObjectMapper(), - clock, - saltGenerator - ); - } - - private static InMemoryAuthFullAccountRepository fullAccountRepository(RoleUkeyBindingEntity... bindings) { - InMemoryAuthFullAccountRepository repository = new InMemoryAuthFullAccountRepository(); - for (RoleUkeyBindingEntity binding : bindings) { - repository.save(fullAccount( - RoleCode.valueOf(binding.getRoleCode()), - binding.getUid(), - binding.getRoleCode().toLowerCase() + "-full-" + binding.getUid(), - "HASH:12345678:SALT", - "SALT" - )); - } - return repository; - } } diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java index 8ab533f..0b70b72 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java @@ -2,34 +2,27 @@ package com.cisd.tms.modules.auth.service; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; -import com.cisd.tms.modules.auth.dto.CaptchaResponse; import com.cisd.tms.modules.auth.dto.CurrentUserResponse; import com.cisd.tms.modules.auth.dto.LoginRequest; import com.cisd.tms.modules.auth.dto.LoginResponse; import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest; -import com.cisd.tms.modules.auth.dto.PasswordLoginRequest; import com.cisd.tms.modules.auth.dto.UkeyLoginProof; -import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest; -import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; import com.cisd.tms.modules.auth.dto.UkeyLoginRequest; import com.cisd.tms.modules.auth.entity.AuthFullAccountEntity; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; -import com.cisd.tms.modules.auth.entity.AuthUserEntity; -import com.cisd.tms.modules.auth.entity.AuthUserAccountEntity; import com.cisd.tms.modules.auth.entity.RoleAccountEntity; import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.AuthMethod; import com.cisd.tms.modules.auth.enums.RoleAccountStatus; import com.cisd.tms.modules.auth.enums.RoleCode; -import com.cisd.tms.modules.auth.repository.AuthSessionRepository; import com.cisd.tms.modules.auth.repository.AuthFullAccountRepository; -import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository; -import com.cisd.tms.modules.auth.repository.AuthUserRepository; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; import com.cisd.tms.modules.auth.repository.RoleAccountRepository; import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; import com.cisd.tms.modules.auth.service.impl.AuthPolicyServiceImpl; import com.cisd.tms.modules.auth.service.impl.AuthServiceImpl; +import com.cisd.tms.modules.auth.service.impl.InMemoryCaptchaService; import com.cisd.tms.modules.mk.enums.MasterKeyStatus; import com.cisd.tms.modules.mk.service.LmkService; import com.fasterxml.jackson.databind.ObjectMapper; @@ -51,26 +44,17 @@ class AuthServiceTest { private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); @Test - void shouldCreateLimitedSessionForSingleAccountRoleLogin() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + void shouldCreateLimitedSessionFromUnifiedRoleSeatPassword() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); - userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null, true)); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - sessions, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "token-limited-001" - ); + roles.save(activeRole(RoleCode.AUDIT_ADMIN)); + accounts.save(activeAccount(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "12345678", "SALT-A", true)); + AuthService service = newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "token-limited-001"); LoginRequest request = new LoginRequest(); request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode()); - request.setAccounts(List.of(account("audit-admin-01", "12345678"))); + request.setAccounts(List.of(passwordAccount(1, "12345678"))); LoginResponse response = service.login(request); @@ -80,634 +64,122 @@ class AuthServiceTest { Assertions.assertTrue(response.getNeedChangePassword()); AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow(); Assertions.assertEquals(AuthMethod.PASSWORD.name(), session.getAuthMethod()); - Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]", session.getAuthenticatedPrincipalsJson()); + Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":1,\"username\":\"audit-admin-01\"}]", session.getAuthenticatedPrincipalsJson()); } @Test - void shouldRequirePasswordChangeWhenLimitedAccountPasswordIsExpired() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); - AuthUserAccountEntity account = activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null); - account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0)); - userAccounts.save(account); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - new InMemoryAuthSessionRepository(), - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "token-limited-expired-001" - ); - - LoginRequest request = new LoginRequest(); - request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode()); - request.setAccounts(List.of(account("audit-admin-01", "12345678"))); - - LoginResponse response = service.login(request); - - Assertions.assertEquals("token-limited-expired-001", response.getToken()); - Assertions.assertTrue(response.getNeedChangePassword()); - } - - @Test - void shouldRequirePasswordChangeWhenFullAccountPasswordIsExpired() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); - AuthFullAccountEntity account = activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 0, null); - account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0)); - fullAccounts.save(account); - ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); - - AuthService service = newAuthService( - roleAccounts, - new InMemoryAuthUserAccountRepository(), - fullAccounts, - new InMemoryAuthSessionRepository(), - ukeyBindings, - FIXED_CLOCK, - () -> "token-full-expired-001" - ); - - LoginRequest request = new LoginRequest(); - request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); - request.setUkeySerials(List.of("UK-1")); - request.setFullAccounts(List.of(fullAccount(1, "12345678"))); - - LoginResponse response = service.login(request); - - Assertions.assertEquals("token-full-expired-001", response.getToken()); - Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); - Assertions.assertTrue(response.getNeedChangePassword()); - } - - @Test - void shouldRequireTwoAccountsForSuperAdminPasswordLogin() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN)); - userAccounts.save(activeUserAccount("super-admin-01", RoleCode.SUPER_ADMIN, "11111111", "SALT-S1", 0, null)); - userAccounts.save(activeUserAccount("super-admin-02", RoleCode.SUPER_ADMIN, "22222222", "SALT-S2", 0, null)); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - new InMemoryAuthSessionRepository(), - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "unused" - ); - - LoginRequest request = new LoginRequest(); - request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); - request.setAccounts(List.of(account("super-admin-01", "11111111"))); - - BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); - - Assertions.assertEquals("SUPER_ADMIN requires exactly 2 account passwords", exception.getMessage()); - } - - @Test - void shouldLockOnlySpecificUserAccountAfterFiveWrongPasswords() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN)); - userAccounts.save(activeUserAccount("super-admin-01", RoleCode.SUPER_ADMIN, "11111111", "SALT-S1", 4, null)); - userAccounts.save(activeUserAccount("super-admin-02", RoleCode.SUPER_ADMIN, "22222222", "SALT-S2", 0, null)); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - new InMemoryAuthSessionRepository(), - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "unused" - ); - - LoginRequest request = new LoginRequest(); - request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); - request.setAccounts(List.of( - account("super-admin-01", "bad-password"), - account("super-admin-02", "22222222") - )); - - BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); - - Assertions.assertEquals("user account is locked", exception.getMessage()); - AuthUserAccountEntity locked = userAccounts.findByUsername("super-admin-01").orElseThrow(); - AuthUserAccountEntity untouched = userAccounts.findByUsername("super-admin-02").orElseThrow(); - Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), locked.getStatus()); - Assertions.assertEquals(5, locked.getFailedCount()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), locked.getLockedUntil()); - Assertions.assertEquals(0, untouched.getFailedCount()); - Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), untouched.getStatus()); - } - - @Test - void shouldKeepMeEndpointRoleScoped() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); + void shouldUseSamePasswordSourceForPasswordAndUkeyLogin() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); - userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null, true)); - AuthSessionEntity session = session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()); - session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]"); - sessions.save(session); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - sessions, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "unused" - ); - - CurrentUserResponse response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-001"); - - Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getUsername()); - Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getDisplayName(), response.getDisplayName()); - Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole()); - Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel()); - Assertions.assertTrue(response.getNeedChangePassword()); - } - - @Test - void shouldExposePasswordExpiredFlagFromMeEndpoint() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN)); - AuthUserAccountEntity account = activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null); - account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0)); - userAccounts.save(account); - AuthSessionEntity session = session("token-me-expired-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" - ); - - CurrentUserResponse response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-expired-001"); - - Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole()); - Assertions.assertTrue(response.getNeedChangePassword()); - } - - @Test - void shouldChangeCurrentFullAccountPasswordForActiveSession() { - 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, true)); - AuthSessionEntity session = session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); - session.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":null}]"); - sessions.save(session); - - AuthService service = newAuthService( - roleAccounts, - new InMemoryAuthUserAccountRepository(), - fullAccounts, - sessions, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "unused" - ); - - service.changeFullAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); - - AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); - Assertions.assertEquals("salt-test", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:Abc1234!: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.getPasswordChangedAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt()); - } - - @Test - void shouldChangeCurrentLimitedAccountPasswordForActiveSession() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - 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", "Abc1234!"); - - AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow(); - Assertions.assertEquals("salt-test", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:Abc1234!: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.getPasswordChangedAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt()); - } - - @Test - void shouldRejectCurrentPasswordChangeWhenNewPasswordIsTooSimple() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - 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" - ); - - BizException exception = Assertions.assertThrows( - BizException.class, - () -> service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321") - ); - - Assertions.assertEquals("密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含", exception.getMessage()); - } - - @Test - void shouldThrowSessionInvalidWhenChangingFullAccountPasswordWithExpiredSession() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); - fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "FULL-SALT-K", 0, null)); - AuthSessionEntity expiredSession = session("token-expired-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); - expiredSession.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":null}]"); - expiredSession.setExpiresAt(LocalDateTime.of(2026, 3, 23, 1, 59)); - sessions.save(expiredSession); - - AuthService service = newAuthService( - roleAccounts, - new InMemoryAuthUserAccountRepository(), - fullAccounts, - sessions, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - () -> "unused" - ); - - BizException exception = Assertions.assertThrows(BizException.class, - () -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "Abc1234!")); - - Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode()); - Assertions.assertEquals("session expired", exception.getMessage()); - } - - @Test - void shouldIssueCaptchaAndAcceptPasswordLoginWithValidCaptcha() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - roleAccounts.save(activeRole(RoleCode.OPS_ADMIN)); - userAccounts.save(activeUserAccount("ops-admin-01", RoleCode.OPS_ADMIN, "12345678", "SALT-O", 0, null)); - LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); - org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); - InMemoryCaptchaService captchaService = new InMemoryCaptchaService(); - - AuthService service = new AuthServiceImpl( - roleAccounts, - new InMemoryLegacyAuthUserRepository(), - userAccounts, - new InMemoryAuthFullAccountRepository(), - new InMemoryAuthSessionRepository(), - new InMemoryRoleUkeyBindingRepository(), - new FakePasswordHasher(), - lmkService, - org.mockito.Mockito.mock(UkeyLoginRandomService.class), - org.mockito.Mockito.mock(CompatUkeyVerifier.class), - captchaService, - new ObjectMapper(), - FIXED_CLOCK, - () -> "token-password-002", - () -> "salt-password-002", - new AuthPolicyServiceImpl() - ); - - CaptchaResponse captcha = service.issueCaptcha(); - - PasswordLoginRequest request = new PasswordLoginRequest(); - request.setRoleCode(RoleCode.OPS_ADMIN.getCode()); - request.setCaptchaId(captcha.getCaptchaId()); - request.setCaptchaCode("ABCD"); - request.setAccounts(List.of(account("ops-admin-01", "12345678"))); - - LoginResponse response = service.passwordLogin(request); - - Assertions.assertEquals("token-password-002", response.getToken()); - Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel()); - } - - @Test - void shouldKickPreviousLimitedSessionForSameRoleAndSameAccounts() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN)); - userAccounts.save(activeUserAccount("super-admin-01", RoleCode.SUPER_ADMIN, "11111111", "SALT-S1", 0, null)); - userAccounts.save(activeUserAccount("super-admin-02", RoleCode.SUPER_ADMIN, "22222222", "SALT-S2", 0, null)); - - AuthSessionEntity previous = session("token-limited-old", RoleCode.SUPER_ADMIN.getCode(), AuthLevel.LIMITED.name()); - previous.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-01\"},{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-02\"}]"); - sessions.save(previous); - - AuthService service = newAuthService( - roleAccounts, - userAccounts, - new InMemoryAuthFullAccountRepository(), - sessions, - new InMemoryRoleUkeyBindingRepository(), - FIXED_CLOCK, - new SequenceSessionTokenGenerator("token-limited-new") - ); - - LoginRequest request = new LoginRequest(); - request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); - request.setAccounts(List.of( - account("super-admin-02", "22222222"), - account("super-admin-01", "11111111") - )); - - LoginResponse response = service.login(request); - - Assertions.assertEquals("token-limited-new", response.getToken()); - AuthSessionEntity expired = sessions.findBySessionToken("token-limited-old").orElseThrow(); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getLogoutAt()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), expired.getExpiresAt()); - AuthSessionEntity current = sessions.findBySessionToken("token-limited-new").orElseThrow(); - Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-01\"},{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"super-admin-02\"}]", current.getAuthenticatedPrincipalsJson()); - } - - @Test - void shouldKickPreviousFullSessionForSameRole() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); - InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); - 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")); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(activeRole(RoleCode.KEY_ADMIN)); + accounts.save(activeAccount(RoleCode.KEY_ADMIN, 1, "key-admin-01", "Abc1234!", "SALT-K", false)); + bindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class); CompatUkeyVerifier verifier = org.mockito.Mockito.mock(CompatUkeyVerifier.class); org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); - org.mockito.Mockito.when(randomService.issue(RoleCode.KEY_ADMIN.getCode(), 1)).thenReturn(List.of("RB-1")); - AuthService service = new AuthServiceImpl( - roleAccounts, - new InMemoryLegacyAuthUserRepository(), - userAccounts, - fullAccounts, - sessions, - ukeyBindings, - new FakePasswordHasher(), - lmkService, - randomService, - verifier, - new InMemoryCaptchaService(), - new ObjectMapper(), - FIXED_CLOCK, - () -> "token-full-ukey-002", - () -> "salt-full-ukey-002", - new AuthPolicyServiceImpl() - ); + AuthService service = newAuthService(roles, accounts, sessions, bindings, lmkService, randomService, verifier, () -> "token-001"); - UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest(); - randomRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode()); - UkeyLoginRandomResponse ignored = service.issueUkeyLoginRandoms(randomRequest); - Assertions.assertNotNull(ignored); + LoginRequest passwordRequest = new LoginRequest(); + passwordRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + passwordRequest.setAccounts(List.of(passwordAccount(1, "Abc1234!"))); + Assertions.assertEquals(AuthLevel.LIMITED.name(), service.login(passwordRequest).getAuthLevel()); - UkeyLoginRequest request = new UkeyLoginRequest(); - request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); - request.setLoginFactors(List.of( - proofWithPassword("PUB-1", 1, "12345678", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1") - )); + UkeyLoginRequest ukeyRequest = new UkeyLoginRequest(); + ukeyRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + ukeyRequest.setLoginFactors(List.of(proofWithPassword("PUB-1", 1, "Abc1234!", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"))); + LoginResponse response = service.ukeyLogin(ukeyRequest); - LoginResponse response = service.ukeyLogin(request); - - Assertions.assertEquals("token-full-ukey-002", response.getToken()); Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); - org.mockito.Mockito.verify(randomService).assertIssued(RoleCode.KEY_ADMIN.getCode(), List.of("RB-1")); org.mockito.Mockito.verify(verifier).verifyIssuedBinding( "{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"KEY_ADMIN\",\"uid\":1}", "ISSUE-1" ); - org.mockito.Mockito.verify(verifier).verifyLoginSignature("PUB-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"); } @Test - void shouldLockFullAccountDuringUkeyLoginWhenSeatPasswordIsWrong() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); - InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); - roleAccounts.save(activeRole(RoleCode.KEY_ADMIN)); - fullAccounts.save(activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 4, null)); - ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); + void shouldChangeUnifiedPasswordFromLimitedSessionAndAffectUkeyLogin() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(activeRole(RoleCode.KEY_ADMIN)); + accounts.save(activeAccount(RoleCode.KEY_ADMIN, 1, "key-admin-01", "Old1234!", "SALT-K", true)); + bindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1")); + AuthSessionEntity session = session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.LIMITED.name()); + session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":1,\"username\":\"key-admin-01\"}]"); + sessions.save(session); - 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 = newAuthService(roles, accounts, sessions, bindings, () -> "token-after-change"); - 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-ukey-003", - new AuthPolicyServiceImpl() - ); + service.changeAccountPassword("token-change-001", 1, "Old1234!", "New1234!"); - UkeyLoginRequest request = new UkeyLoginRequest(); - request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); - request.setLoginFactors(List.of( - proofWithPassword("PUB-1", 1, "bad-password", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1") - )); + AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); + Assertions.assertEquals("salt-test", changed.getPasswordSalt()); + Assertions.assertEquals("HASH:New1234!:salt-test", changed.getPasswordHash()); + Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword())); + } - BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request)); + @Test + void shouldShareWrongPasswordLockStateAcrossLoginMethods() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + roles.save(activeRole(RoleCode.SUPER_ADMIN)); + accounts.save(activeAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "11111111", "SALT-S1", 4, null)); + accounts.save(activeAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "22222222", "SALT-S2", 0, null)); + + AuthService service = newAuthService(roles, accounts, new InMemoryAuthSessionRepository(), new InMemoryRoleUkeyBindingRepository(), () -> "unused"); + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + request.setAccounts(List.of(passwordAccount(1, "bad-password"), passwordAccount(2, "22222222"))); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); Assertions.assertEquals("full account is locked", exception.getMessage()); - AuthFullAccountEntity updated = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); - Assertions.assertEquals(5, updated.getFailedCount()); - Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus()); - Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil()); + AuthFullAccountEntity locked = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); + AuthFullAccountEntity untouched = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), locked.getStatus()); + Assertions.assertEquals(5, locked.getFailedCount()); + Assertions.assertEquals(0, untouched.getFailedCount()); } @Test - void shouldLoginSuperAdminWithPerUidPasswordsDuringUkeyLogin() { - InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); - InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository(); - InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository(); + void shouldExposePasswordChangeFlagFromMeForLimitedSessionUid() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = 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")); + roles.save(activeRole(RoleCode.AUDIT_ADMIN)); + AuthFullAccountEntity account = activeAccount(RoleCode.AUDIT_ADMIN, 1, "audit-admin-01", "12345678", "SALT-A", false); + account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0)); + accounts.save(account); + AuthSessionEntity session = session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name()); + session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":1,\"username\":\"audit-admin-01\"}]"); + sessions.save(session); - 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")); + CurrentUserResponse response = newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused") + .me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-001"); - 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" - ); + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole()); + Assertions.assertTrue(response.getNeedChangePassword()); } @Test void shouldLoginSuperAdminWithBackupUkeysBoundToSameUidSeats() { - 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, 1, "UK-1-BACKUP", "PUB-1-BACKUP")); - ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2", "PUB-2")); - ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2-BACKUP", "PUB-2-BACKUP")); + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + roles.save(activeRole(RoleCode.SUPER_ADMIN)); + accounts.save(activeAccount(RoleCode.SUPER_ADMIN, 1, "super-admin-01", "11111111", "SALT-S1", false)); + accounts.save(activeAccount(RoleCode.SUPER_ADMIN, 2, "super-admin-02", "22222222", "SALT-S2", false)); + bindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1", "PUB-1")); + bindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1-BACKUP", "PUB-1-BACKUP")); + bindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2", "PUB-2")); + bindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2-BACKUP", "PUB-2-BACKUP")); LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class); @@ -715,25 +187,7 @@ class AuthServiceTest { 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, - sessions, - ukeyBindings, - new FakePasswordHasher(), - lmkService, - randomService, - verifier, - new InMemoryCaptchaService(), - new ObjectMapper(), - FIXED_CLOCK, - () -> "token-full-super-backup-001", - () -> "salt-full-super-backup-001", - new AuthPolicyServiceImpl() - ); - + AuthService service = newAuthService(roles, accounts, new InMemoryAuthSessionRepository(), bindings, lmkService, randomService, verifier, () -> "token-backup-001"); UkeyLoginRequest request = new UkeyLoginRequest(); request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); request.setLoginFactors(List.of( @@ -743,44 +197,67 @@ class AuthServiceTest { LoginResponse response = service.ukeyLogin(request); - Assertions.assertEquals("token-full-super-backup-001", response.getToken()); + Assertions.assertEquals("token-backup-001", response.getToken()); Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); 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-BACKUP\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":1}", - "ISSUE-1B" - ); - org.mockito.Mockito.verify(verifier).verifyIssuedBinding( - "{\"pubKey\":\"PUB-2-BACKUP\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":2}", - "ISSUE-2B" - ); } @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")); + void shouldRejectPasswordChangeWithExpiredSession() { + InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); + InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + roles.save(activeRole(RoleCode.KEY_ADMIN)); + accounts.save(activeAccount(RoleCode.KEY_ADMIN, 1, "key-admin-01", "12345678", "SALT-K", false)); + AuthSessionEntity expired = session("token-expired-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name()); + expired.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":\"key-admin-01\"}]"); + expired.setExpiresAt(LocalDateTime.of(2026, 3, 23, 1, 59)); + sessions.save(expired); + BizException exception = Assertions.assertThrows(BizException.class, + () -> newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused") + .changeAccountPassword("token-expired-001", 1, "12345678", "Abc1234!")); + + Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode()); + Assertions.assertEquals("session expired", exception.getMessage()); + } + + private static AuthService newAuthService( + InMemoryRoleAccountRepository roles, + InMemoryAuthFullAccountRepository accounts, + InMemoryAuthSessionRepository sessions, + InMemoryRoleUkeyBindingRepository bindings, + SessionTokenGenerator tokenGenerator + ) { 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"); + return newAuthService( + roles, + accounts, + sessions, + bindings, + lmkService, + org.mockito.Mockito.mock(UkeyLoginRandomService.class), + org.mockito.Mockito.mock(CompatUkeyVerifier.class), + tokenGenerator + ); + } - AuthService service = new AuthServiceImpl( - roleAccounts, - new InMemoryLegacyAuthUserRepository(), - userAccounts, - fullAccounts, - new InMemoryAuthSessionRepository(), - ukeyBindings, + private static AuthService newAuthService( + InMemoryRoleAccountRepository roles, + InMemoryAuthFullAccountRepository accounts, + InMemoryAuthSessionRepository sessions, + InMemoryRoleUkeyBindingRepository bindings, + LmkService lmkService, + UkeyLoginRandomService randomService, + CompatUkeyVerifier verifier, + SessionTokenGenerator tokenGenerator + ) { + return new AuthServiceImpl( + roles, + accounts, + sessions, + bindings, new FakePasswordHasher(), lmkService, randomService, @@ -788,28 +265,10 @@ class AuthServiceTest { new InMemoryCaptchaService(), new ObjectMapper(), FIXED_CLOCK, - () -> "unused", - () -> "salt-full-super-002", + tokenGenerator, + () -> "salt-test", 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) { @@ -822,42 +281,18 @@ class AuthServiceTest { return entity; } - private static AuthUserAccountEntity activeUserAccount( - String username, + private static AuthFullAccountEntity activeAccount( RoleCode roleCode, + int uid, + String accountName, String password, String salt, - int failedCount, - LocalDateTime lockedUntil - ) { - return activeUserAccount(username, roleCode, password, salt, failedCount, lockedUntil, false); - } - - private static AuthUserAccountEntity activeUserAccount( - String username, - RoleCode roleCode, - String password, - String salt, - int failedCount, - LocalDateTime lockedUntil, boolean needChangePassword ) { - AuthUserAccountEntity entity = new AuthUserAccountEntity(); - entity.setId((long) (username.hashCode() & Integer.MAX_VALUE)); - entity.setUsername(username); - entity.setDisplayName(username); - entity.setRoleCode(roleCode.getCode()); - entity.setPasswordSalt(salt); - entity.setPasswordHash("HASH:" + password + ":" + salt); - entity.setStatus(RoleAccountStatus.ACTIVE.name()); - entity.setNeedChangePassword(needChangePassword); - entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0)); - entity.setFailedCount(failedCount); - entity.setLockedUntil(lockedUntil); - return entity; + return activeAccount(roleCode, uid, accountName, password, salt, 0, null, needChangePassword); } - private static AuthFullAccountEntity activeFullAccount( + private static AuthFullAccountEntity activeAccount( RoleCode roleCode, int uid, String accountName, @@ -866,10 +301,10 @@ class AuthServiceTest { int failedCount, LocalDateTime lockedUntil ) { - return activeFullAccount(roleCode, uid, accountName, password, salt, failedCount, lockedUntil, false); + return activeAccount(roleCode, uid, accountName, password, salt, failedCount, lockedUntil, false); } - private static AuthFullAccountEntity activeFullAccount( + private static AuthFullAccountEntity activeAccount( RoleCode roleCode, int uid, String accountName, @@ -895,28 +330,16 @@ class AuthServiceTest { return entity; } - private static PasswordLoginAccountRequest account(String username, String password) { + private static PasswordLoginAccountRequest passwordAccount(int uid, String password) { PasswordLoginAccountRequest request = new PasswordLoginAccountRequest(); - request.setUsername(username); - request.setPassword(password); - return request; - } - - private static com.cisd.tms.modules.auth.dto.FullLoginAccountRequest fullAccount(int uid, String password) { - com.cisd.tms.modules.auth.dto.FullLoginAccountRequest request = new com.cisd.tms.modules.auth.dto.FullLoginAccountRequest(); request.setUid(uid); request.setPassword(password); return request; } - private static RoleUkeyBindingEntity activeBinding( - RoleCode roleCode, - int uid, - String serial, - String pubKey - ) { + private static RoleUkeyBindingEntity activeBinding(RoleCode roleCode, int uid, String serial, String pubKey) { RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity(); - entity.setId((long) uid); + entity.setId((long) Math.abs((roleCode.getCode() + uid + serial).hashCode())); entity.setRoleCode(roleCode.getCode()); entity.setUid(uid); entity.setUkeySerial(serial); @@ -925,24 +348,6 @@ class AuthServiceTest { return entity; } - private static UkeyLoginProof proof( - String pubKey, - int uid, - String serverRandom, - String issueSignature, - String loginPayload, - String loginSignature - ) { - UkeyLoginProof proof = new UkeyLoginProof(); - proof.setPubKey(pubKey); - proof.setUid(uid); - proof.setServerRandom(serverRandom); - proof.setIssueSignature(issueSignature); - proof.setLoginPayload(loginPayload); - proof.setLoginSignature(loginSignature); - return proof; - } - private static UkeyLoginProof proofWithPassword( String pubKey, int uid, @@ -952,8 +357,14 @@ class AuthServiceTest { String loginPayload, String loginSignature ) { - UkeyLoginProof proof = proof(pubKey, uid, serverRandom, issueSignature, loginPayload, loginSignature); + UkeyLoginProof proof = new UkeyLoginProof(); + proof.setPubKey(pubKey); + proof.setUid(uid); proof.setPassword(password); + proof.setServerRandom(serverRandom); + proof.setIssueSignature(issueSignature); + proof.setLoginPayload(loginPayload); + proof.setLoginSignature(loginSignature); return proof; } @@ -969,37 +380,7 @@ class AuthServiceTest { return entity; } - private static AuthService newAuthService( - InMemoryRoleAccountRepository roleAccounts, - InMemoryAuthUserAccountRepository userAccounts, - InMemoryAuthFullAccountRepository fullAccounts, - InMemoryAuthSessionRepository sessions, - InMemoryRoleUkeyBindingRepository ukeyBindings, - Clock clock, - SessionTokenGenerator tokenGenerator - ) { - return new AuthServiceImpl( - roleAccounts, - new InMemoryLegacyAuthUserRepository(), - userAccounts, - fullAccounts, - sessions, - ukeyBindings, - new FakePasswordHasher(), - org.mockito.Mockito.mock(LmkService.class), - org.mockito.Mockito.mock(UkeyLoginRandomService.class), - org.mockito.Mockito.mock(CompatUkeyVerifier.class), - new InMemoryCaptchaService(), - new ObjectMapper(), - clock, - tokenGenerator, - () -> "salt-test", - new AuthPolicyServiceImpl() - ); - } - private static class FakePasswordHasher implements PasswordHasher { - @Override public String hash(String password, String salt) { return "HASH:" + password + ":" + salt; @@ -1011,23 +392,6 @@ class AuthServiceTest { } } - private static class SequenceSessionTokenGenerator implements SessionTokenGenerator { - private final List tokens; - private int index = 0; - - private SequenceSessionTokenGenerator(String... tokens) { - this.tokens = List.of(tokens); - } - - @Override - public String nextToken() { - if (index >= tokens.size()) { - return tokens.get(tokens.size() - 1); - } - return tokens.get(index++); - } - } - private static class InMemoryRoleAccountRepository implements RoleAccountRepository { private final Map store = new ConcurrentHashMap<>(); @@ -1047,33 +411,6 @@ class AuthServiceTest { } } - private static class InMemoryAuthUserAccountRepository implements AuthUserAccountRepository { - private final Map store = new ConcurrentHashMap<>(); - - @Override - public Optional findByUsername(String username) { - return Optional.ofNullable(store.get(username)); - } - - @Override - public List findByRoleCode(String roleCode) { - return store.values().stream() - .filter(entity -> roleCode.equals(entity.getRoleCode())) - .sorted(Comparator.comparing(AuthUserAccountEntity::getUsername)) - .toList(); - } - - @Override - public void save(AuthUserAccountEntity entity) { - store.put(entity.getUsername(), entity); - } - - @Override - public void update(AuthUserAccountEntity entity) { - store.put(entity.getUsername(), entity); - } - } - private static class InMemoryAuthFullAccountRepository implements AuthFullAccountRepository { private final Map store = new ConcurrentHashMap<>(); @@ -1097,15 +434,7 @@ class AuthServiceTest { @Override public void update(AuthFullAccountEntity entity) { - store.put(entity.getRoleCode() + "#" + entity.getUid(), entity); - } - } - - private static class InMemoryLegacyAuthUserRepository implements AuthUserRepository { - - @Override - public Optional findByUsername(String username) { - return Optional.empty(); + save(entity); } } @@ -1117,6 +446,16 @@ class AuthServiceTest { return Optional.ofNullable(store.get(sessionToken)); } + @Override + public void save(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + + @Override + public void update(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + @Override public List findActiveByRoleCodeAndAuthLevel(String roleCode, String authLevel, LocalDateTime now) { return store.values().stream() @@ -1134,82 +473,38 @@ class AuthServiceTest { String authenticatedPrincipalsJson, LocalDateTime now ) { - return store.values().stream() - .filter(entity -> roleCode.equals(entity.getRoleCode())) - .filter(entity -> authLevel.equals(entity.getAuthLevel())) + return findActiveByRoleCodeAndAuthLevel(roleCode, authLevel, now).stream() .filter(entity -> java.util.Objects.equals(authenticatedPrincipalsJson, entity.getAuthenticatedPrincipalsJson())) - .filter(entity -> entity.getLogoutAt() == null) - .filter(entity -> entity.getExpiresAt() != null && entity.getExpiresAt().isAfter(now)) .toList(); } - - @Override - public void save(AuthSessionEntity entity) { - store.put(entity.getSessionToken(), entity); - } - - @Override - public void update(AuthSessionEntity entity) { - store.put(entity.getSessionToken(), entity); - } } private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { - private final Map> store = new ConcurrentHashMap<>(); + private final List store = new ArrayList<>(); @Override - public List findActiveByRoleCodeAndUid(String roleCode, Integer uid) { - return store.getOrDefault(roleCode, List.of()).stream() - .filter(entity -> uid.equals(entity.getUid())) + public List findActiveByRoleCode(String roleCode) { + return store.stream() + .filter(entity -> roleCode.equals(entity.getRoleCode())) .filter(entity -> "ACTIVE".equals(entity.getStatus())) - .sorted(Comparator.comparing(RoleUkeyBindingEntity::getBoundAt).reversed()) .toList(); } @Override - public List findActiveByRoleCode(String roleCode) { - return store.getOrDefault(roleCode, List.of()).stream() - .filter(entity -> "ACTIVE".equals(entity.getStatus())) - .sorted(Comparator.comparing(RoleUkeyBindingEntity::getUid)) + public List findActiveByRoleCodeAndUid(String roleCode, Integer uid) { + return findActiveByRoleCode(roleCode).stream() + .filter(entity -> uid.equals(entity.getUid())) .toList(); } @Override public void save(RoleUkeyBindingEntity entity) { - store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()).add(entity); + store.add(entity); } @Override public void update(RoleUkeyBindingEntity entity) { - List list = store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()); - list.removeIf(item -> item.getId().equals(entity.getId())); - list.add(entity); - } - } - - private static class InMemoryCaptchaService implements CaptchaService { - private final Map store = new ConcurrentHashMap<>(); - - @Override - public CaptchaResponse issueCaptcha() { - return issue("ABCD"); - } - - private CaptchaResponse issue(String code) { - String captchaId = "captcha-" + store.size(); - store.put(captchaId, code); - CaptchaResponse response = new CaptchaResponse(); - response.setCaptchaId(captchaId); - response.setImageBase64("mock-image"); - return response; - } - - @Override - public void verify(String captchaId, String captchaCode) { - String expected = store.remove(captchaId); - if (expected == null || !expected.equalsIgnoreCase(captchaCode)) { - throw new BizException(com.cisd.tms.common.enums.ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed"); - } + save(entity); } } } diff --git a/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java b/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java index a694101..6cee0eb 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/impl/PcieCompatUkeyVerifierTest.java @@ -49,6 +49,55 @@ class PcieCompatUkeyVerifierTest { verifier().verifyLoginSignature(rawPointHex, payload, rawSignatureHex); } + @Test + void shouldVerifyLoginSignatureWithGm0018EccRefPublicKeyBase64() throws Exception { + KeyPair keyPair = generateSm2KeyPair(); + String payload = "LOGIN-DATA-GM0018"; + String eccRefPublicKeyBase64 = Base64.getEncoder().encodeToString(gm0018EccRefPublicKey((ECPublicKey) keyPair.getPublic())); + String signatureBase64 = Base64.getEncoder().encodeToString(gm0018Signature(signDer(payload, keyPair))); + + verifier().verifyLoginSignature(eccRefPublicKeyBase64, payload, signatureBase64); + } + + @Test + void shouldParseProvidedGm0018EccRefPublicKeyBase64() { + String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm"; + String rawZeroSignatureHex = "00".repeat(64); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", rawZeroSignatureHex) + ); + + Assertions.assertEquals("login signature verification failed", exception.getMessage()); + } + + @Test + void shouldRejectProvidedGm0018LoginFactorsWhenSignatureDoesNotMatchPayload() { + String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm"; + String payload = "{\"ra\":\"5893784655680600\",\"rb\":\"2005fe7a2df071c378f3198de5eb2138\",\"role\":\"SUPER_ADMIN\",\"uid\":\"2\",\"rid\":\"SUPER_ADMIN\",\"block\":\"58937846556806002005fe7a2df071c378f3198de5eb2138SUPER_ADMIN\"}"; + String signature = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC19q5+THL9c3za11LSV3onnloEogld9DOkkAJodr9aUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnvomyeYPgfTcGnqXYZwHez6rkwNyhtXtlFpVLFg9SUk="; + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier().verifyLoginSignature(pubKey, payload, signature) + ); + + Assertions.assertEquals("login signature verification failed", exception.getMessage()); + } + + @Test + void shouldReportMissingLoginSignatureAfterProvidedGm0018PublicKeyIsParsed() { + String pubKey = "AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA09yf/T4UEoEnHOFslOQWh+oArfbK+RWK0jfZ5yOfPDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADOo3fWGXgwtVeodmyfNHPZwz5SnqjMSCzVKtnc322mm"; + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", null) + ); + + Assertions.assertEquals("login signature is required", exception.getMessage()); + } + @Test void shouldRejectWhenLoginSignatureDoesNotMatch() throws Exception { KeyPair keyPair = generateSm2KeyPair(); @@ -91,6 +140,27 @@ class PcieCompatUkeyVerifierTest { return result; } + private byte[] gm0018EccRefPublicKey(ECPublicKey publicKey) { + byte[] x = toFixed32(publicKey.getW().getAffineX().toByteArray()); + byte[] y = toFixed32(publicKey.getW().getAffineY().toByteArray()); + byte[] result = new byte[132]; + result[0] = 0x00; + result[1] = 0x01; + result[2] = 0x00; + result[3] = 0x00; + System.arraycopy(x, 0, result, 4 + 32, 32); + System.arraycopy(y, 0, result, 4 + 64 + 32, 32); + return result; + } + + private byte[] gm0018Signature(byte[] derSignature) throws Exception { + byte[] rawSignature = derToRawRs(derSignature); + byte[] result = new byte[128]; + System.arraycopy(rawSignature, 0, result, 32, 32); + System.arraycopy(rawSignature, 32, result, 96, 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); diff --git a/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectLoginTest.java b/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectLoginTest.java index fd458fe..4c420e5 100644 --- a/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectLoginTest.java +++ b/src/test/java/com/cisd/tms/modules/log/aspect/OperationAuditAspectLoginTest.java @@ -42,7 +42,7 @@ class OperationAuditAspectLoginTest { PasswordLoginRequest request = new PasswordLoginRequest(); request.setRoleCode("AUDIT_ADMIN"); PasswordLoginAccountRequest account = new PasswordLoginAccountRequest(); - account.setUsername("audit-admin-01"); + account.setUid(1); request.setAccounts(List.of(account)); LoginResponse response = new LoginResponse(); response.setRoleCode("AUDIT_ADMIN"); @@ -59,7 +59,7 @@ class OperationAuditAspectLoginTest { Assertions.assertEquals(OperatorRoleCode.AUDIT_ADMIN, command.getOperatorRoleCode()); Assertions.assertEquals(AuthLevel.LIMITED, command.getOperatorAuthLevel()); Assertions.assertEquals(OperationResult.SUCCESS, command.getOperationResult()); - Assertions.assertEquals("口令登录 [role=AUDIT_ADMIN, principal=audit-admin-01]", command.getSummary()); + Assertions.assertEquals("口令登录 [role=AUDIT_ADMIN, principal=uid=1]", command.getSummary()); Assertions.assertEquals("10.0.0.8", command.getRemoteIp()); } diff --git a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java index 968ffc4..277b09d 100644 --- a/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java +++ b/src/test/java/com/cisd/tms/security/internal/ReplayProtectedEndpointsTest.java @@ -35,10 +35,8 @@ class ReplayProtectedEndpointsTest { @Test void shouldProtectSensitiveControllersAndMethods() throws Exception { - Assertions.assertNotNull(annotation(AuthController.class, "changeFullAccountPassword", + Assertions.assertNotNull(annotation(AuthController.class, "changeAccountPassword", 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.assertNull(AnnotatedElementUtils.findMergedAnnotation(AuthAdminController.class, ReplayProtected.class));