fix:设备信息

This commit is contained in:
waner
2026-05-13 15:33:29 +08:00
parent 4b886aa9c2
commit c3465dbb2a
19 changed files with 333 additions and 42 deletions
+1 -1
View File
@@ -297,7 +297,7 @@ Open:
- `db/TMS.sql` - `db/TMS.sql`
- `db/CMEP.sql`(包内存在时) - `db/CMEP.sql`(包内存在时)
- `db/<orgCode>.sql`(标准/间参收发器包内存在时,导回同名机构库) - `db/<orgCode>.sql`(标准/间参收发器包内存在时,导回同名机构库)
- `TMS_DB` 备份默认通过 `tms.backup.tms-db-excluded-tables` 排除角色、授权、会话、审计和资源备份/恢复任务表,恢复时不会覆盖新机器安全状态,也不会把旧的 `RUNNING` 任务带到新机器 - `TMS_DB` 备份默认通过 `tms.backup.tms-db-excluded-tables` 排除角色、授权、会话和资源备份/恢复任务表,恢复时不会覆盖新机器安全登录状态,也不会把旧的 `RUNNING` 任务带到新机器;`tms_operation_audit_log` 默认随 TMS 库一起备份
- `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本 - `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本
- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置;健康检查会按配置重试等待 TMS 真正启动完成 - Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置;健康检查会按配置重试等待 TMS 真正启动完成
@@ -44,7 +44,7 @@
### 2.3 权限和防重放 ### 2.3 权限和防重放
资源备份/恢复接口要求内部登录态,角色为 `OPS_ADMIN` 或 `SUPER_ADMIN`,认证等级为 `FULL`。 资源备份/恢复接口要求内部登录态,角色为 `OPS_ADMIN`,认证等级为 `FULL`。
需要防重放头的接口: 需要防重放头的接口:
@@ -514,7 +514,7 @@ Content-Disposition: attachment; filename="RBKP-20260429-160000-ABCDEF.tmsbak"
- `USER_KEY` - `USER_KEY`
- `MQ_REPLAY` - `MQ_REPLAY`
`TMS_DB` 默认排除 `tms.backup.tms-db-excluded-tables` 中声明的表。默认排除角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。 `TMS_DB` 默认排除 `tms.backup.tms-db-excluded-tables` 中声明的表。默认排除角色、授权、会话和资源任务表,避免恢复时覆盖新机器安全登录状态或带回旧的资源任务状态;`tms_operation_audit_log` 默认随 TMS 库一起备份。
## 6. TypeScript 类型建议 ## 6. TypeScript 类型建议
@@ -840,5 +840,5 @@ const latestText = computed(() => {
- apply 返回后不要继续请求恢复步骤接口;恢复侧没有步骤接口。 - apply 返回后不要继续请求恢复步骤接口;恢复侧没有步骤接口。
- TMS 重启后只调用 latest 展示最近恢复结果。 - TMS 重启后只调用 latest 展示最近恢复结果。
- `payloadDecrypted=false` 不表示失败,它表示预检阶段没有解密完整 payload。 - `payloadDecrypted=false` 不表示失败,它表示预检阶段没有解密完整 payload。
- `TMS_DB` 默认不包含角色、授权、会话和审计表,因此恢复不会覆盖新机器已有登录授权状态。 - `TMS_DB` 默认不包含角色、授权、会话和资源任务表,因此恢复不会覆盖新机器已有登录授权状态;操作审计日志默认随 TMS 库备份。
- 服务端路径类字段不展示给普通用户,只用于运维排查。 - 服务端路径类字段不展示给普通用户,只用于运维排查。
@@ -95,12 +95,18 @@ public class WebMvcConfig implements WebMvcConfigurer {
.addPathPatterns("/api/**") .addPathPatterns("/api/**")
.excludePathPatterns( .excludePathPatterns(
"/api/v1/device/status", "/api/v1/device/status",
"/api/v1/device/info",
"/api/v1/masterKey/status", "/api/v1/masterKey/status",
"/api/v1/masterKey/init",
"/api/v1/masterKey/recover",
"/api/v1/auth/password-login", "/api/v1/auth/password-login",
"/api/v1/system/health",
"/api/v1/device/time-config",
"/api/v1/auth/ukey-login", "/api/v1/auth/ukey-login",
"/api/v1/auth/ukey-login/randoms", "/api/v1/auth/ukey-login/randoms",
"/api/v1/auth/captcha", "/api/v1/auth/captcha",
"/api/v1/auth/super-admin/ukeys/issue-sign" "/api/v1/auth/super-admin/ukeys/issue-sign",
"/api/v1/device/network/**"
); );
registry.addInterceptor(internalApiReplayInterceptor) registry.addInterceptor(internalApiReplayInterceptor)
@@ -33,7 +33,6 @@ public class ResourceBackupProperties {
"tms_auth_session", "tms_auth_session",
"tms_auth_challenge", "tms_auth_challenge",
"tms_auth_audit_log", "tms_auth_audit_log",
"tms_operation_audit_log",
"tms_resource_backup_task", "tms_resource_backup_task",
"tms_resource_restore_task" "tms_resource_restore_task"
); );
@@ -69,7 +69,7 @@ public class ResourceRestoreController {
} }
@GetMapping("/latest") @GetMapping("/latest")
@RequireInternalAuth(anyRole = RoleCode.OPS_ADMIN, authLevel = AuthLevel.FULL) @RequireInternalAuth(anyRole = RoleCode.OPS_ADMIN, authLevel = AuthLevel.LIMITED)
@Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。") @Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。")
public ApiResponse<ResourceRestoreLatestResponse> latestRestore() { public ApiResponse<ResourceRestoreLatestResponse> latestRestore() {
return ApiResponse.success(resourceTaskQueryService.getLatestRestore()); return ApiResponse.success(resourceTaskQueryService.getLatestRestore());
@@ -16,6 +16,9 @@ import com.cisd.tms.modules.device.support.SystemHardwareProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.service.InitService; import com.cisd.tms.modules.init.service.InitService;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -49,7 +52,7 @@ public class DeviceProfileServiceImpl implements DeviceProfileService {
section.setModel(deviceProfileProperties.getModel()); section.setModel(deviceProfileProperties.getModel());
section.setName(deviceProfileProperties.getName()); section.setName(deviceProfileProperties.getName());
section.setSerialNumber(trim(info.getDeviceSerial())); section.setSerialNumber(trim(info.getDeviceSerial()));
section.setDeviceVersion(formatVersion(info.getStandardVersion())); section.setDeviceVersion(firstNonBlank(info.getDeviceVersionText(), formatVersion(info.getDeviceVersion())));
return section; return section;
} }
@@ -136,12 +139,34 @@ public class DeviceProfileServiceImpl implements DeviceProfileService {
if (version <= 0) { if (version <= 0) {
return ""; return "";
} }
String decoded = decodeAsciiDeviceVersion(version);
if (!decoded.isBlank()) {
return decoded.startsWith("V") ? decoded : "V" + decoded;
}
if (version >= 100) { if (version >= 100) {
return "V" + (version / 100) + "." + String.format("%02d", version % 100); return "V" + (version / 100) + "." + String.format("%02d", version % 100);
} }
return "V" + version; return "V" + version;
} }
private String decodeAsciiDeviceVersion(int version) {
byte[] bytes = ByteBuffer.allocate(Integer.BYTES)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(version)
.array();
String decoded = new String(bytes, StandardCharsets.US_ASCII)
.replace("\u0000", "")
.trim();
if (!decoded.isBlank() && decoded.chars().allMatch(DeviceProfileServiceImpl::isPrintableVersionChar)) {
return decoded;
}
return "";
}
private static boolean isPrintableVersionChar(int value) {
return (value >= '0' && value <= '9') || value == '.' || value == 'V' || value == 'v';
}
private static String firstNonBlank(String... values) { private static String firstNonBlank(String... values) {
if (values == null) { if (values == null) {
return ""; return "";
@@ -3,12 +3,29 @@ package com.cisd.tms.modules.device.support;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class DefaultHardwareStatusProbe implements HardwareStatusProbe { public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
private final String diskUsagePath;
private final CommandRunner commandRunner;
@Autowired
public DefaultHardwareStatusProbe(@Value("${tms.device.runtime-status.disk-usage-path:/home/tms}") String diskUsagePath) {
this(diskUsagePath, DefaultHardwareStatusProbe::runShellCommand);
}
DefaultHardwareStatusProbe(String diskUsagePath, CommandRunner commandRunner) {
this.diskUsagePath = firstNonBlank(diskUsagePath, "/home/tms");
this.commandRunner = commandRunner == null ? DefaultHardwareStatusProbe::runShellCommand : commandRunner;
}
@Override @Override
public Double cpuUsagePercent() { public Double cpuUsagePercent() {
CpuTimes before = readCpuTimes(); CpuTimes before = readCpuTimes();
@@ -52,8 +69,48 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
@Override @Override
public UsageSnapshot diskUsage() { public UsageSnapshot diskUsage() {
double totalGb = physicalDiskTotalGb();
double usedGb = filesystemUsedGb();
return new UsageSnapshot(round(usedGb), round(totalGb));
}
private double physicalDiskTotalGb() {
try { try {
List<String> lines = runCommand("df -k /"); List<String> lines = commandRunner.run("lsblk -b -d -n -e 7,11 -o TYPE,SIZE");
long totalBytes = 0L;
for (String line : lines) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2 && "disk".equals(parts[0])) {
totalBytes += Long.parseLong(parts[1]);
}
}
if (totalBytes <= 0L) {
throw new IllegalStateException("物理磁盘探测未返回数据");
}
return totalBytes / 1024D / 1024D / 1024D;
} catch (IOException ex) {
throw new IllegalStateException("physical disk probe 执行失败", ex);
}
}
private double filesystemUsedGb() {
RuntimeException lastFailure = null;
for (String path : diskUsagePaths()) {
try {
return parseFilesystemUsedGb(commandRunner.run("df -kP " + shellQuote(path)));
} catch (IOException ex) {
lastFailure = new IllegalStateException("disk probe 执行失败", ex);
} catch (RuntimeException ex) {
lastFailure = ex;
}
}
if (lastFailure != null) {
throw lastFailure;
}
throw new IllegalStateException("disk probe 执行失败");
}
private double parseFilesystemUsedGb(List<String> lines) {
if (lines.size() < 2) { if (lines.size() < 2) {
throw new IllegalStateException("磁盘探测未返回数据"); throw new IllegalStateException("磁盘探测未返回数据");
} }
@@ -61,12 +118,15 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
if (parts.length < 5) { if (parts.length < 5) {
throw new IllegalStateException("磁盘探测返回无效数据"); throw new IllegalStateException("磁盘探测返回无效数据");
} }
double totalGb = Long.parseLong(parts[1]) / 1024D / 1024D; return Long.parseLong(parts[2]) / 1024D / 1024D;
double usedGb = Long.parseLong(parts[2]) / 1024D / 1024D;
return new UsageSnapshot(round(usedGb), round(totalGb));
} catch (IOException ex) {
throw new IllegalStateException("disk probe 执行失败", ex);
} }
private List<String> diskUsagePaths() {
LinkedHashSet<String> paths = new LinkedHashSet<>();
paths.add(firstNonBlank(diskUsagePath, "/home/tms"));
paths.add("/home");
paths.add("/");
return new ArrayList<>(paths);
} }
private CpuTimes readCpuTimes() { private CpuTimes readCpuTimes() {
@@ -92,7 +152,7 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
return Long.parseLong(parts[1]); return Long.parseLong(parts[1]);
} }
private List<String> runCommand(String command) throws IOException { private static List<String> runShellCommand(String command) throws IOException {
Process process = new ProcessBuilder("bash", "-lc", command).start(); Process process = new ProcessBuilder("bash", "-lc", command).start();
try { try {
int exitCode = process.waitFor(); int exitCode = process.waitFor();
@@ -107,10 +167,23 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
} }
} }
private static String firstNonBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private static String shellQuote(String value) {
return "'" + value.replace("'", "'\"'\"'") + "'";
}
private double round(double value) { private double round(double value) {
return Math.round(value * 100D) / 100D; return Math.round(value * 100D) / 100D;
} }
@FunctionalInterface
interface CommandRunner {
List<String> run(String command) throws IOException;
}
private record CpuTimes(long idle, long total) { private record CpuTimes(long idle, long total) {
} }
} }
@@ -1,16 +1,25 @@
package com.cisd.tms.modules.device.support; package com.cisd.tms.modules.device.support;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class DefaultSystemHardwareProbe implements SystemHardwareProbe { public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
private final CommandRunner commandRunner;
public DefaultSystemHardwareProbe() {
this(DefaultSystemHardwareProbe::runShellCommand);
}
DefaultSystemHardwareProbe(CommandRunner commandRunner) {
this.commandRunner = commandRunner == null ? DefaultSystemHardwareProbe::runShellCommand : commandRunner;
}
@Override @Override
public String cpuModel() { public String cpuModel() {
return firstLine("lscpu | awk -F: '/Model name/ {print $2}'"); return firstLine("lscpu | awk -F: '/Model name/ {print $2}'");
@@ -39,17 +48,32 @@ public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
@Override @Override
public String diskTotal() { public String diskTotal() {
return firstLine("df -h / | awk 'NR==2 {print $2}'"); try {
List<String> lines = commandRunner.run("lsblk -b -d -n -e 7,11 -o TYPE,SIZE");
long totalBytes = 0L;
for (String line : lines) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2 && "disk".equals(parts[0])) {
totalBytes += Long.parseLong(parts[1]);
}
}
if (totalBytes <= 0L) {
return "";
}
double gb = totalBytes / 1024.0 / 1024.0 / 1024.0;
return String.format(Locale.ROOT, "%.2fGB", gb);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return "";
} catch (IOException | NumberFormatException ignored) {
return "";
}
} }
private String firstLine(String command) { private String firstLine(String command) {
try { try {
Process process = new ProcessBuilder("bash", "-lc", command).start(); List<String> lines = commandRunner.run(command);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { return lines.isEmpty() ? "" : lines.get(0).trim();
String line = reader.readLine();
process.waitFor();
return line == null ? "" : line.trim();
}
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return ""; return "";
@@ -57,4 +81,19 @@ public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
return ""; return "";
} }
} }
private static List<String> runShellCommand(String command) throws IOException, InterruptedException {
Process process = new ProcessBuilder("bash", "-lc", command).start();
int exitCode = process.waitFor();
List<String> lines = new String(process.getInputStream().readAllBytes()).lines().toList();
if (exitCode != 0) {
throw new IOException("命令执行失败: " + command);
}
return lines;
}
@FunctionalInterface
interface CommandRunner {
List<String> run(String command) throws IOException, InterruptedException;
}
} }
@@ -4,6 +4,7 @@ import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.file.dto.FileDetailResponse; import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse; import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.service.FileService; import com.cisd.tms.modules.file.service.FileService;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +27,7 @@ public class FileController {
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。 // 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
@PostMapping("/upload") @PostMapping("/upload")
@ReplayProtected
@Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。") @Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。")
public ApiResponse<FileUploadResponse> upload( public ApiResponse<FileUploadResponse> upload(
@Parameter(description = "待上传的初始化文件") @Parameter(description = "待上传的初始化文件")
@@ -37,7 +37,6 @@ public class LmkController {
@PostMapping("/masterKey/init") @PostMapping("/masterKey/init")
@Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。") @Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。")
@ReplayProtected
public ApiResponse<MasterKeyStateResult> initMasterKey() { public ApiResponse<MasterKeyStateResult> initMasterKey() {
return ApiResponse.success(lmkService.initMasterKey()); return ApiResponse.success(lmkService.initMasterKey());
} }
@@ -65,7 +64,6 @@ public class LmkController {
*/ */
@PostMapping("/masterKey/recover") @PostMapping("/masterKey/recover")
@Operation(summary = "恢复全部密钥", description = "请求体需传入两份按 packetIndex 标识的备份包。两份备份包必须分别对应 packetIndex=1、2。服务端会依次恢复 LMK、IK 和用户密钥。") @Operation(summary = "恢复全部密钥", description = "请求体需传入两份按 packetIndex 标识的备份包。两份备份包必须分别对应 packetIndex=1、2。服务端会依次恢复 LMK、IK 和用户密钥。")
@ReplayProtected
public ApiResponse<Void> recoverLmkEx(@Valid @RequestBody MasterKeyRecoverPacketsRequest request) { public ApiResponse<Void> recoverLmkEx(@Valid @RequestBody MasterKeyRecoverPacketsRequest request) {
validatePacketIndexes(request.getPackets()); validatePacketIndexes(request.getPackets());
lmkService.recoverKeyPackets(request.getPackets()); lmkService.recoverKeyPackets(request.getPackets());
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.mk.service.impl; package com.cisd.tms.modules.mk.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.common.util.DivisionUtils; import com.cisd.tms.common.util.DivisionUtils;
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType; import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType;
@@ -70,6 +72,9 @@ public class LmkServiceImpl implements LmkService {
@Override @Override
public MasterKeyStateResult initMasterKey() { public MasterKeyStateResult initMasterKey() {
if (pcieCryptoService.checkLmk()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "主密钥已存在,拒绝重新初始化");
}
// 密钥体系一旦重建,之前导出的两份备份包就全部失效,必须清空当前轮次缓存。 // 密钥体系一旦重建,之前导出的两份备份包就全部失效,必须清空当前轮次缓存。
clearCachedBackupRound(); clearCachedBackupRound();
byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64());
@@ -171,6 +176,9 @@ public class LmkServiceImpl implements LmkService {
@Override @Override
public MasterKeyStateResult recoverKeyPackets(List<MasterKeyBackupPacket> packets) { public MasterKeyStateResult recoverKeyPackets(List<MasterKeyBackupPacket> packets) {
if (pcieCryptoService.checkLmk()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "主密钥已存在,拒绝重新初始化");
}
// 进入恢复流程后,旧的备份轮次缓存不再可信,避免和恢复后的新密钥体系混用。 // 进入恢复流程后,旧的备份轮次缓存不再可信,避免和恢复后的新密钥体系混用。
clearCachedBackupRound(); clearCachedBackupRound();
// 调用方可以任意顺序传入两份包,这里统一按 packetIndex 排序并校验必须是 1、2 两包。 // 调用方可以任意顺序传入两份包,这里统一按 packetIndex 排序并校验必须是 1、2 两包。
@@ -28,6 +28,7 @@ class ResourceBackupPropertiesTest {
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_account")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_account"));
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_ukey_binding")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_ukey_binding"));
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_auth_session")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_auth_session"));
Assertions.assertFalse(properties.getTmsDbExcludedTables().contains("tms_operation_audit_log"));
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_resource_backup_task")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_resource_backup_task"));
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_resource_restore_task")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_resource_restore_task"));
Assertions.assertEquals(300, properties.getMysqldumpTimeoutSeconds()); Assertions.assertEquals(300, properties.getMysqldumpTimeoutSeconds());
@@ -13,17 +13,17 @@ import org.junit.jupiter.api.Test;
class ResourceBackupAuthorizationAuditContractTest { class ResourceBackupAuthorizationAuditContractTest {
@Test @Test
void shouldRequireOpsOrSuperAdminFullForBackupAndRestoreEndpoints() throws Exception { void shouldRequireOpsAdminFullForBackupAndRestoreEndpoints() throws Exception {
assertFullOpsOrSuperAdmin(ResourceBackupController.class.getMethod( assertFullOpsAdmin(ResourceBackupController.class.getMethod(
"createBackupTask", "createBackupTask",
com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest.class com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest.class
)); ));
assertFullOpsOrSuperAdmin(ResourceBackupController.class.getMethod("downloadPackage", String.class)); assertFullOpsAdmin(ResourceBackupController.class.getMethod("downloadPackage", String.class));
assertFullOpsOrSuperAdmin(ResourceRestoreController.class.getMethod( assertFullOpsAdmin(ResourceRestoreController.class.getMethod(
"precheck", "precheck",
com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest.class com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest.class
)); ));
assertFullOpsOrSuperAdmin(ResourceRestoreController.class.getMethod( assertFullOpsAdmin(ResourceRestoreController.class.getMethod(
"createRestoreTask", "createRestoreTask",
com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.class com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.class
)); ));
@@ -46,11 +46,11 @@ class ResourceBackupAuthorizationAuditContractTest {
), ActionType.RECOVER); ), ActionType.RECOVER);
} }
private static void assertFullOpsOrSuperAdmin(Method method) { private static void assertFullOpsAdmin(Method method) {
RequireInternalAuth auth = method.getAnnotation(RequireInternalAuth.class); RequireInternalAuth auth = method.getAnnotation(RequireInternalAuth.class);
Assertions.assertNotNull(auth, method.getName() + " should require internal auth"); Assertions.assertNotNull(auth, method.getName() + " should require internal auth");
Assertions.assertEquals(AuthLevel.FULL, auth.authLevel()); Assertions.assertEquals(AuthLevel.FULL, auth.authLevel());
Assertions.assertArrayEquals(new RoleCode[]{RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, auth.anyRole()); Assertions.assertArrayEquals(new RoleCode[]{RoleCode.OPS_ADMIN}, auth.anyRole());
} }
private static void assertAudit(Method method, ActionType actionType) { private static void assertAudit(Method method, ActionType actionType) {
@@ -103,6 +103,7 @@ class MysqlDatabaseBackupCollectorTest {
collector.collectTmsDatabaseDump(); collector.collectTmsDatabaseDump();
String invocation = Files.readString(argsLog); String invocation = Files.readString(argsLog);
Assertions.assertFalse(invocation.contains("--ignore-table=TMS.tms_operation_audit_log"));
Assertions.assertTrue(invocation.contains("--ignore-table=TMS.tms_resource_backup_task")); Assertions.assertTrue(invocation.contains("--ignore-table=TMS.tms_resource_backup_task"));
Assertions.assertTrue(invocation.contains("--ignore-table=TMS.tms_resource_restore_task")); Assertions.assertTrue(invocation.contains("--ignore-table=TMS.tms_resource_restore_task"));
} }
@@ -68,8 +68,8 @@ class DeviceProfileServiceTest {
Assertions.assertEquals("SYD7108", response.getDevice().getModel()); Assertions.assertEquals("SYD7108", response.getDevice().getModel());
Assertions.assertEquals("跨境支付终端一体机", response.getDevice().getName()); Assertions.assertEquals("跨境支付终端一体机", response.getDevice().getName());
Assertions.assertEquals("1234567890ABCDEF", response.getDevice().getSerialNumber()); Assertions.assertEquals("709230400244", response.getDevice().getSerialNumber());
Assertions.assertEquals("V2.50", response.getDevice().getDeviceVersion()); Assertions.assertEquals("V1.00", response.getDevice().getDeviceVersion());
Assertions.assertEquals("SYD1408加密卡", response.getHardware().getCryptoCardName()); Assertions.assertEquals("SYD1408加密卡", response.getHardware().getCryptoCardName());
Assertions.assertEquals("Hygon C86-3G (OPN:3350)", response.getHardware().getCpuModel()); Assertions.assertEquals("Hygon C86-3G (OPN:3350)", response.getHardware().getCpuModel());
Assertions.assertEquals("32GB", response.getHardware().getMemoryTotal()); Assertions.assertEquals("32GB", response.getHardware().getMemoryTotal());
@@ -290,9 +290,10 @@ class DeviceProfileServiceTest {
public DeviceInfoResult getDeviceInfo() { public DeviceInfoResult getDeviceInfo() {
DeviceInfoResult result = new DeviceInfoResult(); DeviceInfoResult result = new DeviceInfoResult();
result.setDeviceName("SYD1408加密卡"); result.setDeviceName("SYD1408加密卡");
result.setDeviceSerial("1234567890ABCDEF"); result.setDeviceSerial("709230400244");
result.setDeviceVersion(100); result.setDeviceVersion(808463921);
result.setStandardVersion(250); result.setStandardVersion(1581074);
result.setStandardVersionText("GMT 0018-2012");
return result; return result;
} }
}; };
@@ -0,0 +1,20 @@
package com.cisd.tms.modules.device.support;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
class DefaultHardwareStatusProbeSpringTest {
@Test
void shouldInstantiateDefaultHardwareStatusProbeBean() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.registerBean(PropertySourcesPlaceholderConfigurer.class, PropertySourcesPlaceholderConfigurer::new);
context.register(DefaultHardwareStatusProbe.class);
context.refresh();
Assertions.assertNotNull(context.getBean(DefaultHardwareStatusProbe.class));
}
}
}
@@ -0,0 +1,64 @@
package com.cisd.tms.modules.device.support;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class DefaultHardwareStatusProbeTest {
@Test
void shouldUsePhysicalDiskTotalForDiskUsageTotal() {
List<String> commands = new ArrayList<>();
DefaultHardwareStatusProbe probe = new DefaultHardwareStatusProbe("/home/tms", command -> {
commands.add(command);
if ("lsblk -b -d -n -e 7,11 -o TYPE,SIZE".equals(command)) {
return List.of(
"disk 1024000000000",
"disk 1024000000000"
);
}
return List.of(
"Filesystem 1024-blocks Used Available Capacity Mounted on",
"/dev/mapper/data 1073741824 524288000 549453824 49% /home"
);
});
UsageSnapshot snapshot = probe.diskUsage();
Assertions.assertEquals(500.0, snapshot.getUsedGb());
Assertions.assertEquals(1907.35, snapshot.getTotalGb());
Assertions.assertEquals(
List.of("lsblk -b -d -n -e 7,11 -o TYPE,SIZE", "df -kP '/home/tms'"),
commands
);
}
@Test
void shouldFallbackToHomeWhenConfiguredPathForUsedCapacityIsUnavailable() {
List<String> commands = new ArrayList<>();
DefaultHardwareStatusProbe probe = new DefaultHardwareStatusProbe("/home/tms", command -> {
commands.add(command);
if ("lsblk -b -d -n -e 7,11 -o TYPE,SIZE".equals(command)) {
return List.of("disk 966367641600");
}
if ("df -kP '/home/tms'".equals(command)) {
throw new IOException("No such file or directory");
}
return List.of(
"Filesystem 1024-blocks Used Available Capacity Mounted on",
"/dev/mapper/data 943718400 471859200 471859200 50% /home"
);
});
UsageSnapshot snapshot = probe.diskUsage();
Assertions.assertEquals(450.0, snapshot.getUsedGb());
Assertions.assertEquals(900.0, snapshot.getTotalGb());
Assertions.assertEquals(
List.of("lsblk -b -d -n -e 7,11 -o TYPE,SIZE", "df -kP '/home/tms'", "df -kP '/home'"),
commands
);
}
}
@@ -0,0 +1,37 @@
package com.cisd.tms.modules.device.support;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class DefaultSystemHardwareProbeTest {
@Test
void shouldReturnPhysicalDiskTotalInGbUsingBinaryUnits() {
List<String> commands = new ArrayList<>();
DefaultSystemHardwareProbe probe = new DefaultSystemHardwareProbe(command -> {
commands.add(command);
return List.of(
"disk 1024000000000",
"disk 1024000000000",
"rom 1048576"
);
});
String total = probe.diskTotal();
Assertions.assertEquals("1907.35GB", total);
Assertions.assertEquals(List.of("lsblk -b -d -n -e 7,11 -o TYPE,SIZE"), commands);
}
@Test
void shouldReturnBlankWhenPhysicalDiskProbeFails() {
DefaultSystemHardwareProbe probe = new DefaultSystemHardwareProbe(command -> {
throw new IOException("lsblk unavailable");
});
Assertions.assertEquals("", probe.diskTotal());
}
}
@@ -14,6 +14,8 @@ import com.cisd.tms.integration.crypto.pcie.model.MasterKeyRecoveryRequest;
import com.cisd.tms.integration.crypto.pcie.model.MasterKeyRecoveryResult; import com.cisd.tms.integration.crypto.pcie.model.MasterKeyRecoveryResult;
import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest; import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart;
@@ -69,6 +71,21 @@ class LmkServiceTest {
Assertions.assertEquals("主密钥初始化失败", exception.getMessage()); Assertions.assertEquals("主密钥初始化失败", exception.getMessage());
} }
@Test
void shouldRejectInitMasterKeyWhenMasterKeyAlreadyExists() {
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
BizException exception = Assertions.assertThrows(BizException.class, service::initMasterKey);
Assertions.assertEquals(ErrorCode.CONFLICT.getCode(), exception.getCode());
Assertions.assertEquals("主密钥已存在,拒绝重新初始化", exception.getMessage());
Mockito.verify(pcieCryptoService, Mockito.never()).initializeMasterKey(Mockito.any(), Mockito.any());
}
@Test @Test
void shouldDestroyMasterKeyAfterDeletingUserKey() { void shouldDestroyMasterKeyAfterDeletingUserKey() {
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);