diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/PasswordChangePrepareResponse.java b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordChangePrepareResponse.java new file mode 100644 index 0000000..e49627e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordChangePrepareResponse.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "修改口令准备响应") +public class PasswordChangePrepareResponse { + + @Schema(description = "改密确认令牌") + private String changeToken; + + @Schema(description = "旧口令动态摘要挑战随机数") + private String oldServerRandom; + + @Schema(description = "新口令盐值,前端写入 UKey") + private String newPasswordSalt; + + public String getChangeToken() { + return changeToken; + } + + public void setChangeToken(String changeToken) { + this.changeToken = changeToken; + } + + public String getOldServerRandom() { + return oldServerRandom; + } + + public void setOldServerRandom(String oldServerRandom) { + this.oldServerRandom = oldServerRandom; + } + + public String getNewPasswordSalt() { + return newPasswordSalt; + } + + public void setNewPasswordSalt(String newPasswordSalt) { + this.newPasswordSalt = newPasswordSalt; + } +} diff --git a/src/test/java/com/cisd/tms/modules/log/controller/AuditedOperationEndpointsTest.java b/src/test/java/com/cisd/tms/modules/log/controller/AuditedOperationEndpointsTest.java new file mode 100644 index 0000000..0e3037a --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/log/controller/AuditedOperationEndpointsTest.java @@ -0,0 +1,152 @@ +package com.cisd.tms.modules.log.controller; + +import com.cisd.tms.modules.auth.controller.AuthAdminController; +import com.cisd.tms.modules.auth.controller.AuthController; +import com.cisd.tms.modules.device.controller.DeviceController; +import com.cisd.tms.modules.device.controller.IpWhitelistController; +import com.cisd.tms.modules.device.controller.NetworkConfigController; +import com.cisd.tms.modules.device.controller.TimeConfigController; +import com.cisd.tms.modules.log.annotation.AuditedOperation; +import com.cisd.tms.modules.log.enums.ActionType; +import com.cisd.tms.modules.log.enums.ModuleCode; +import com.cisd.tms.modules.mk.controller.LmkController; +import com.cisd.tms.modules.openapi.service.controller.OpenApiController; +import com.cisd.tms.modules.upgrade.controller.UpgradeController; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AnnotatedElementUtils; + +import java.lang.reflect.Method; + +class AuditedOperationEndpointsTest { + + @Test + void shouldAuditAuthStateChanges() throws Exception { + assertAudit(AuthController.class, "passwordLogin", ModuleCode.AUTH, ActionType.LOGIN, + com.cisd.tms.modules.auth.dto.PasswordLoginRequest.class); + assertAudit(AuthController.class, "ukeyLogin", ModuleCode.AUTH, ActionType.LOGIN, + com.cisd.tms.modules.auth.dto.UkeyLoginRequest.class); + assertAudit(AuthController.class, "logout", ModuleCode.AUTH, ActionType.LOGOUT, + jakarta.servlet.http.HttpServletRequest.class); + assertAudit(AuthController.class, "changeAccountPassword", ModuleCode.AUTH, ActionType.UPDATE, + Integer.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class); + + assertAudit(AuthAdminController.class, "resetPassword", ModuleCode.AUTH, ActionType.RESET, + String.class, jakarta.servlet.http.HttpServletRequest.class); + assertAudit(AuthAdminController.class, "changeAccountPassword", ModuleCode.AUTH, ActionType.UPDATE, + String.class, Integer.class, com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest.class, + jakarta.servlet.http.HttpServletRequest.class); + assertAudit(AuthAdminController.class, "issueSuperAdminUkeyBindingSign", ModuleCode.AUTH, ActionType.BIND, + com.cisd.tms.modules.mk.dto.UKeySignDTO.class); + assertAudit(AuthAdminController.class, "issueUkeyBindingSign", ModuleCode.AUTH, ActionType.BIND, + String.class, com.cisd.tms.modules.mk.dto.UKeySignDTO.class, jakarta.servlet.http.HttpServletRequest.class); + } + + @Test + void shouldAuditDeviceNetworkAndMasterKeyMutations() throws Exception { + assertAudit(DeviceController.class, "restart", ModuleCode.DEVICE, ActionType.RESTART, + jakarta.servlet.http.HttpServletRequest.class); + assertAudit(TimeConfigController.class, "configureTime", ModuleCode.DEVICE, ActionType.UPDATE, + com.cisd.tms.modules.device.dto.TimeConfigRequest.class); + + assertAudit(NetworkConfigController.class, "configureNetworkCard", ModuleCode.NETWORK, ActionType.UPDATE, + com.cisd.tms.modules.device.dto.network.NetworkConfigureRequest.class); + assertAudit(NetworkConfigController.class, "addNetworkCard", ModuleCode.NETWORK, ActionType.CREATE, + com.cisd.tms.modules.device.dto.network.NetworkConfigureRequest.class); + assertAudit(NetworkConfigController.class, "deleteNetworkCardRecord", ModuleCode.NETWORK, ActionType.DELETE, + com.cisd.tms.modules.device.dto.network.NetworkDeleteRequest.class); + assertAudit(NetworkConfigController.class, "deleteBond", ModuleCode.NETWORK, ActionType.DELETE, String.class); + assertAudit(NetworkConfigController.class, "createBond", ModuleCode.NETWORK, ActionType.CREATE, + com.cisd.tms.modules.device.dto.network.BondCreateRequest.class); + assertAudit(NetworkConfigController.class, "configBond", ModuleCode.NETWORK, ActionType.UPDATE, + com.cisd.tms.modules.device.dto.network.BondConfigRequest.class); + assertAudit(NetworkConfigController.class, "routeAdd", ModuleCode.NETWORK, ActionType.CREATE, + com.cisd.tms.modules.device.dto.network.RouteCreateRequest.class); + assertAudit(NetworkConfigController.class, "routeUpdate", ModuleCode.NETWORK, ActionType.UPDATE, + com.cisd.tms.modules.device.dto.network.RouteUpdateRequest.class); + assertAudit(NetworkConfigController.class, "routeDelete", ModuleCode.NETWORK, ActionType.DELETE, + com.cisd.tms.modules.device.dto.network.RouteDeleteRequest.class); + + assertAudit(IpWhitelistController.class, "addWhitelist", ModuleCode.DEVICE, ActionType.CREATE, + com.cisd.tms.modules.device.dto.network.IpWhitelistRequest.class); + assertAudit(IpWhitelistController.class, "updateWhitelist", ModuleCode.DEVICE, ActionType.UPDATE, + com.cisd.tms.modules.device.dto.network.IpWhitelistRequest.class); + assertAudit(IpWhitelistController.class, "deleteWhitelist", ModuleCode.DEVICE, ActionType.DELETE, Long.class); + + assertAudit(LmkController.class, "initMasterKey", ModuleCode.KEY, ActionType.CREATE); + assertAudit(LmkController.class, "destroyMasterKey", ModuleCode.KEY, ActionType.DELETE); + assertAudit(LmkController.class, "recoverLmkEx", ModuleCode.KEY, ActionType.RECOVER, + com.cisd.tms.modules.mk.dto.MasterKeyRecoverPacketsRequest.class); + assertAudit(LmkController.class, "Activate", ModuleCode.DEVICE, ActionType.ENABLE, + com.cisd.tms.modules.mk.dto.MasterKeyActivateRequest.class); + } + + @Test + void shouldAuditFileUpgradeBackupLogAndOpenApiMutations() throws Exception { + assertAudit(com.cisd.tms.modules.file.controller.FileController.class, "upload", ModuleCode.SYSTEM, ActionType.IMPORT, + org.springframework.web.multipart.MultipartFile.class); + + assertAudit(UpgradeController.class, "uploadPackage", ModuleCode.UPGRADE, ActionType.IMPORT, + org.springframework.web.multipart.MultipartFile.class); + assertAudit(UpgradeController.class, "create", ModuleCode.UPGRADE, ActionType.CREATE, + com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest.class); + assertAudit(UpgradeController.class, "execute", ModuleCode.UPGRADE, ActionType.EXECUTE, String.class); + assertAudit(UpgradeController.class, "rollback", ModuleCode.UPGRADE, ActionType.ROLLBACK, String.class); + + assertAudit(BackupController.class, "backup", ModuleCode.LOG, ActionType.BACKUP); + assertAudit(BackupController.class, "downloadBackupFile", ModuleCode.LOG, ActionType.EXPORT, String.class); + assertAudit(BackupConfigController.class, "saveOrUpdateConfig", ModuleCode.LOG, ActionType.UPDATE, + com.cisd.tms.modules.log.dto.BackupConfigRequest.class); + assertAudit(OperationAuditController.class, "reviewLog", ModuleCode.LOG, ActionType.UPDATE, + String.class, com.cisd.tms.modules.log.dto.AuditLogReviewRequest.class); + assertAudit(AuditLogRestoreController.class, "restoreFromZip", ModuleCode.LOG, ActionType.RECOVER, + org.springframework.web.multipart.MultipartFile.class); + + assertAudit(OpenApiController.class, "rawSign", ModuleCode.KEY, ActionType.EXECUTE, + com.cisd.tms.modules.openapi.service.dto.RawSignRequest.class); + assertAudit(OpenApiController.class, "dettachedSign", ModuleCode.KEY, ActionType.EXECUTE, + com.cisd.tms.modules.openapi.service.dto.DettachedSignRequest.class); + assertAudit(OpenApiController.class, "uploadCert", ModuleCode.KEY, ActionType.IMPORT, + com.cisd.tms.modules.openapi.service.dto.UploadCertRequest.class); + } + + @Test + void shouldNotAuditReadOnlyPreviewAndVerifyEndpoints() throws Exception { + assertNoAudit(UpgradeController.class, "preview", com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest.class); + assertNoAudit(OperationAuditController.class, "queryPage", + com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest.class); + assertNoAudit(OpenApiController.class, "rawVerify", com.cisd.tms.modules.openapi.service.dto.RawVerifyRequest.class); + assertNoAudit(OpenApiController.class, "dettachedVerify", + com.cisd.tms.modules.openapi.service.dto.DettachedVerifyRequest.class); + assertNoAudit(OpenApiController.class, "dettachedVerifySimple", + com.cisd.tms.modules.openapi.service.dto.DettachedVerifyRequest.class); + } + + private static void assertAudit( + Class controller, + String methodName, + ModuleCode moduleCode, + ActionType actionType, + Class... parameterTypes + ) throws Exception { + AuditedOperation annotation = auditedOperation(controller, methodName, parameterTypes); + Assertions.assertNotNull(annotation, controller.getSimpleName() + "#" + methodName); + Assertions.assertEquals(moduleCode, annotation.module(), controller.getSimpleName() + "#" + methodName); + Assertions.assertEquals(actionType, annotation.action(), controller.getSimpleName() + "#" + methodName); + } + + private static void assertNoAudit(Class controller, String methodName, Class... parameterTypes) throws Exception { + Assertions.assertNull(auditedOperation(controller, methodName, parameterTypes), + controller.getSimpleName() + "#" + methodName); + } + + private static AuditedOperation auditedOperation(Class controller, String methodName, Class... parameterTypes) + throws Exception { + Method method = controller.getMethod(methodName, parameterTypes); + AuditedOperation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, AuditedOperation.class); + if (methodAnnotation != null) { + return methodAnnotation; + } + return AnnotatedElementUtils.findMergedAnnotation(controller, AuditedOperation.class); + } +} diff --git a/src/test/java/com/cisd/tms/modules/log/repository/OperationAuditSchemaContractTest.java b/src/test/java/com/cisd/tms/modules/log/repository/OperationAuditSchemaContractTest.java new file mode 100644 index 0000000..0cae4b2 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/log/repository/OperationAuditSchemaContractTest.java @@ -0,0 +1,23 @@ +package com.cisd.tms.modules.log.repository; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +class OperationAuditSchemaContractTest { + + @Test + void shouldAllowAnonymousBootstrapAuditLogRole() throws Exception { + String fullSql = readClasspathResource("db/migration/V2__tms_schema_full.sql"); + + Assertions.assertTrue(fullSql.contains("operator_role_code VARCHAR(64) NULL")); + Assertions.assertFalse(fullSql.contains("operator_role_code VARCHAR(64) NOT NULL")); + } + + private static String readClasspathResource(String path) throws Exception { + ClassPathResource resource = new ClassPathResource(path); + Assertions.assertTrue(resource.exists(), "Missing classpath resource: " + path); + return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/com/cisd/tms/schema/FullSchemaVersionContractTest.java b/src/test/java/com/cisd/tms/schema/FullSchemaVersionContractTest.java new file mode 100644 index 0000000..de0699e --- /dev/null +++ b/src/test/java/com/cisd/tms/schema/FullSchemaVersionContractTest.java @@ -0,0 +1,34 @@ +package com.cisd.tms.schema; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +class FullSchemaVersionContractTest { + + @Test + void shouldRetainV1MigrationAndExposeCurrentV2FullSchema() throws Exception { + String v1Sql = readClasspathResource("db/migration/V1__tms_schema_full.sql"); + String v2Sql = readClasspathResource("db/migration/V2__tms_schema_full.sql"); + + Assertions.assertTrue(v1Sql.contains("ALTER TABLE tms_auth_full_account")); + Assertions.assertTrue(v1Sql.contains("AFTER password_hash")); + Assertions.assertFalse(v1Sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_full_account")); + Assertions.assertTrue(v2Sql.contains("CREATE TABLE IF NOT EXISTS tms_operation_audit_log")); + } + + @Test + void currentV2FullSchemaShouldNotDependOnPatchStatements() throws Exception { + String v2Sql = readClasspathResource("db/migration/V2__tms_schema_full.sql"); + + Assertions.assertFalse(v2Sql.contains("ALTER TABLE")); + Assertions.assertFalse(v2Sql.lines().anyMatch(line -> line.stripLeading().startsWith("UPDATE "))); + } + + private static String readClasspathResource(String path) throws Exception { + ClassPathResource resource = new ClassPathResource(path); + Assertions.assertTrue(resource.exists(), "Missing classpath resource: " + path); + return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/com/cisd/tms/security/internal/InternalBootstrapAuthContractTest.java b/src/test/java/com/cisd/tms/security/internal/InternalBootstrapAuthContractTest.java new file mode 100644 index 0000000..7893bb1 --- /dev/null +++ b/src/test/java/com/cisd/tms/security/internal/InternalBootstrapAuthContractTest.java @@ -0,0 +1,48 @@ +package com.cisd.tms.security.internal; + +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.security.RequireInternalAuth; +import com.cisd.tms.modules.device.controller.NetworkConfigController; +import com.cisd.tms.modules.device.controller.TimeConfigController; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AnnotatedElementUtils; + +class InternalBootstrapAuthContractTest { + + @Test + void shouldRequireOpsFullAfterBootstrapForNetworkAndTimeConfig() throws Exception { + RequireInternalAuth networkAuth = + AnnotatedElementUtils.findMergedAnnotation(NetworkConfigController.class, RequireInternalAuth.class); + RequireInternalAuth timeAuth = AnnotatedElementUtils.findMergedAnnotation( + TimeConfigController.class.getMethod("configureTime", + com.cisd.tms.modules.device.dto.TimeConfigRequest.class), + RequireInternalAuth.class + ); + + assertBootstrapOpsFull(networkAuth); + assertBootstrapOpsFull(timeAuth); + } + + @Test + void shouldNotStaticallyExcludeBootstrapConfigEndpointsFromInternalAuth() throws Exception { + String source = Files.readString( + Path.of("src/main/java/com/cisd/tms/common/config/WebMvcConfig.java"), + StandardCharsets.UTF_8 + ); + + Assertions.assertFalse(source.contains("\"/api/v1/device/time-config\"")); + Assertions.assertFalse(source.contains("\"/api/v1/device/network/**\"")); + } + + private static void assertBootstrapOpsFull(RequireInternalAuth auth) { + Assertions.assertNotNull(auth); + Assertions.assertEquals(RoleCode.OPS_ADMIN, auth.role()); + Assertions.assertEquals(AuthLevel.FULL, auth.authLevel()); + Assertions.assertTrue(auth.allowBeforeMasterKeyInitialized()); + } +}