Merge remote-tracking branch 'origin/V1.00' into V1.00
This commit is contained in:
@@ -103,6 +103,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
"/api/v1/system/health",
|
||||
"/api/v1/device/time-config",
|
||||
"/api/v1/auth/ukey-login",
|
||||
"/api/v1/init/template",
|
||||
"/api/v1/auth/ukey-login/randoms",
|
||||
"/api/v1/auth/captcha",
|
||||
"/api/v1/auth/super-admin/ukeys/issue-sign",
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ResourceBackupProperties {
|
||||
"tms_resource_restore_task"
|
||||
);
|
||||
private String cmepDatabaseName = "CMEP";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8088/actuator/health";
|
||||
private int healthCheckTimeoutSeconds = 5;
|
||||
private int healthCheckMaxWaitSeconds = 180;
|
||||
private int healthCheckRetryIntervalSeconds = 5;
|
||||
|
||||
@@ -49,14 +49,14 @@ public class DeviceController {
|
||||
|
||||
@GetMapping("/profile")
|
||||
@Operation(summary = "查询设备画像", description = "返回设备静态信息,包括设备信息、硬件信息和按初始化方案裁剪后的软件版本信息。")
|
||||
@RequireInternalAuth(role = RoleCode.OPS_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN, RoleCode.KEY_ADMIN, RoleCode.AUDIT_ADMIN}, authLevel = AuthLevel.LIMITED)
|
||||
public ApiResponse<DeviceProfileResponse> profile() {
|
||||
return ApiResponse.success(deviceProfileService.profile());
|
||||
}
|
||||
|
||||
@GetMapping("/runtime-status")
|
||||
@Operation(summary = "查询设备运行状态", description = "返回硬件实时资源使用情况、加密卡状态,以及按初始化配置裁剪后的软件运行状态。")
|
||||
@RequireInternalAuth(role = RoleCode.OPS_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN, RoleCode.KEY_ADMIN, RoleCode.AUDIT_ADMIN}, authLevel = AuthLevel.LIMITED)
|
||||
public ApiResponse<DeviceRuntimeStatusResponse> runtimeStatus() {
|
||||
return ApiResponse.success(deviceRuntimeStatusService.loadRuntimeStatus());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class InitExecutorProperties {
|
||||
private static final String DEFAULT_STANDARD_RUN_USER = "cmep4i";
|
||||
private static final String DEFAULT_STANDARD_VENDOR_STEP_RUNNER_COMMAND =
|
||||
"/usr/local/sbin/tms-run-standard-vendor-step";
|
||||
private static final String DEFAULT_RABBITMQ_CTL_COMMAND =
|
||||
"sudo -n /usr/local/sbin/tms-rabbitmqctl";
|
||||
|
||||
private Mode mode = Mode.SIMULATE;
|
||||
private int timeoutSec = 600;
|
||||
@@ -104,7 +106,7 @@ public class InitExecutorProperties {
|
||||
private String rabbitAdminUsername = "admin";
|
||||
private String rabbitAdminPassword = "admin";
|
||||
// RabbitMQ 管理命令路径
|
||||
private String rabbitmqCtlCommand = "rabbitmqctl";
|
||||
private String rabbitmqCtlCommand = DEFAULT_RABBITMQ_CTL_COMMAND;
|
||||
private String rabbitmqAdminCommand = "/usr/sbin/rabbitmqadmin";
|
||||
// 标准版 RabbitMQ 初始化脚本(创建admin/通道账号、替换队列脚本机构号)
|
||||
private String standardRabbitSetupScriptPath = "";
|
||||
|
||||
@@ -684,6 +684,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
private String resolveRabbitmqCtlCommand() {
|
||||
List<String> candidates = new ArrayList<>();
|
||||
candidates.add(firstNonBlank(properties.getRabbitmqCtlCommand(), "rabbitmqctl"));
|
||||
candidates.add("sudo -n /usr/local/sbin/tms-rabbitmqctl");
|
||||
candidates.add("/usr/local/rabbitmq/sbin/rabbitmqctl");
|
||||
candidates.add("/usr/local/rabbitmq-server/sbin/rabbitmqctl");
|
||||
candidates.add("/usr/sbin/rabbitmqctl");
|
||||
@@ -694,6 +695,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
if (isBlank(candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (candidateHasArguments(candidate)) {
|
||||
if (isRunnableCommandPrefix(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (candidate.contains("/")) {
|
||||
try {
|
||||
Path path = Paths.get(candidate).normalize().toAbsolutePath();
|
||||
@@ -716,6 +723,37 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isRunnableCommandPrefix(String command) {
|
||||
List<String> parts = splitCommandPrefix(command);
|
||||
if (parts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String executable = parts.get(0);
|
||||
if (executable.contains("/")) {
|
||||
try {
|
||||
Path path = Paths.get(executable).normalize().toAbsolutePath();
|
||||
return Files.exists(path) && Files.isRegularFile(path);
|
||||
} catch (InvalidPathException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !isBlank(findExecutableInPath(executable));
|
||||
}
|
||||
|
||||
private boolean candidateHasArguments(String command) {
|
||||
return splitCommandPrefix(command).size() > 1;
|
||||
}
|
||||
|
||||
private List<String> splitCommandPrefix(String command) {
|
||||
String value = normalize(command);
|
||||
if (isBlank(value)) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(value.split("\\s+"))
|
||||
.filter(part -> !isBlank(part))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String findExecutableInPath(String commandName) {
|
||||
String pathValue = System.getenv("PATH");
|
||||
if (isBlank(pathValue)) {
|
||||
@@ -1920,7 +1958,19 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
|
||||
}
|
||||
|
||||
private int runRabbitmqCommand(Path logFile, List<String> command, boolean ignoreFailure) throws IOException, InterruptedException {
|
||||
return runCommand(logFile, command, ignoreFailure, null, buildRabbitmqEnvironment());
|
||||
return runCommand(logFile, expandRabbitmqCommand(command), ignoreFailure, null, buildRabbitmqEnvironment());
|
||||
}
|
||||
|
||||
private List<String> expandRabbitmqCommand(List<String> command) {
|
||||
if (command == null || command.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> expanded = new ArrayList<>(splitCommandPrefix(command.get(0)));
|
||||
if (expanded.isEmpty()) {
|
||||
expanded.add(command.get(0));
|
||||
}
|
||||
expanded.addAll(command.subList(1, command.size()));
|
||||
return expanded;
|
||||
}
|
||||
|
||||
private int runCommand(Path logFile, List<String> command, boolean ignoreFailure) throws IOException, InterruptedException {
|
||||
|
||||
@@ -8,8 +8,8 @@ import lombok.Data;
|
||||
@Schema(description = "日志审计请求参数")
|
||||
public class AuditLogReviewRequest {
|
||||
|
||||
// @Schema(description = "审计管理员签名值", example = "MEUC...")
|
||||
// private String auditSign;
|
||||
@Schema(description = "审计管理员签名值", example = "MEUC")
|
||||
private String auditSign;
|
||||
@Schema(description = "审计结果 (PASS/ REJECT / NEED_VERIFY)", example = "PASS")
|
||||
private AuditResult auditResult;
|
||||
@Schema(description = "审计意见", example = "审核通过")
|
||||
|
||||
@@ -57,7 +57,7 @@ public class OperationAuditLogPageResponse {
|
||||
// @Schema(description = "请求数据体 SM3 摘要", example = "1ab2c3d4e5f6...")
|
||||
// private String payloadHash; // SM3摘要
|
||||
|
||||
@Schema(description = "请求 SM2 签名值", example = "MEUC...")
|
||||
@Schema(description = "SM2 签名值", example = "MEUC...")
|
||||
private String signValue; // SM2签名值
|
||||
|
||||
@Schema(description = "操作实际发生时间", type = "string", example = "2026-04-16 09:10:16")
|
||||
|
||||
@@ -54,8 +54,8 @@ public class OperationAuditLogResponse {
|
||||
// @Schema(description = "请求数据体 SM3 摘要", example = "1ab2c3d4e5f6...")
|
||||
// private String payloadHash; // SM3摘要
|
||||
|
||||
@Schema(description = "请求 SM2 签名值", example = "MEUC...")
|
||||
private String signValue; // SM2签名值
|
||||
@Schema(description = "SM2 签名值", example = "MEUC...")
|
||||
private String signValue; // SM2签名值
|
||||
|
||||
@Schema(description = "操作实际发生时间", type = "string", format = "date-time", example = "2026-04-16 23:59:59")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
|
||||
@@ -3,17 +3,16 @@ package com.cisd.tms.modules.log.service;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
|
||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.dto.*;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
import com.cisd.tms.modules.log.enums.AuditStatus;
|
||||
import com.cisd.tms.modules.log.enums.AuthLevel;
|
||||
import com.cisd.tms.modules.log.enums.OperationResult;
|
||||
import com.cisd.tms.modules.log.enums.OperatorRoleCode;
|
||||
import com.cisd.tms.modules.log.enums.*;
|
||||
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -22,25 +21,37 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OperationAuditService {
|
||||
|
||||
public static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE";
|
||||
public static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL";
|
||||
private static final String PROVIDER = "BC";
|
||||
private static final String SIGNATURE_ALGO = "SM3withSM2";
|
||||
private static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE";
|
||||
private static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OperationAuditService.class);
|
||||
|
||||
|
||||
private final OperationAuditLogRepository operationAuditLogRepository;
|
||||
|
||||
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
|
||||
private final OperationAuditSigner auditSigner;
|
||||
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
public void record(OperationAuditCommand command, boolean isSensitive) {
|
||||
try {
|
||||
@@ -183,45 +194,87 @@ public class OperationAuditService {
|
||||
OperationAuditLogEntity existLog = operationAuditLogRepository.findByLogId(logId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "未找到该logId"));
|
||||
|
||||
// String signValue = trim(req.getAuditSign());
|
||||
// if (signValue.isEmpty()){
|
||||
// throw new IllegalArgumentException("签名值不能为空");
|
||||
// }
|
||||
//
|
||||
// String payload = auditSigner.buildAuditPayload(existLog);
|
||||
//
|
||||
// UserKeyVerifyRequest userKeyVerifyRequest = new UserKeyVerifyRequest();
|
||||
// userKeyVerifyRequest.setKeyIndex(1);
|
||||
// userKeyVerifyRequest.setData(payload.getBytes(StandardCharsets.UTF_8));
|
||||
// userKeyVerifyRequest.setSignature(decodeBlob(signValue, "signature is invalid"));
|
||||
// try {
|
||||
// pcieCryptoService.userKeyVerifyWithSm3(userKeyVerifyRequest);
|
||||
// } catch (BizException ex) {
|
||||
// throw ex;
|
||||
// } catch (RuntimeException ex) {
|
||||
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification 执行失败");
|
||||
// }
|
||||
|
||||
|
||||
|
||||
if (!AuditStatus.PENDING.equals(existLog.getAuditStatus())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "该日志已审计,禁止重复操作");
|
||||
}
|
||||
|
||||
OperationAuditLogEntity updateEntity = new OperationAuditLogEntity();
|
||||
updateEntity.setLogId(existLog.getLogId());
|
||||
updateEntity.setAuditStatus(AuditStatus.REVIEWED);
|
||||
updateEntity.setAuditResult(req.getAuditResult());
|
||||
updateEntity.setAuditComment(trim(req.getAuditComment()));
|
||||
updateEntity.setAuditedBy(auditorUser);
|
||||
updateEntity.setAuditedAt(LocalDateTime.now());
|
||||
String auditSign = trim(req.getAuditSign());
|
||||
if (auditSign.isEmpty()){
|
||||
throw new IllegalArgumentException("签名值不能为空");
|
||||
}
|
||||
|
||||
String signValueSource = buildSignValueSource(existLog, req.getAuditResult());
|
||||
|
||||
|
||||
List<RoleUkeyBindingEntity> list = roleUkeyBindingRepository.findActiveByRoleCode("AUDIT_ADMIN");
|
||||
if (list.isEmpty()){
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "未找到审计管理员UKey");
|
||||
}
|
||||
|
||||
RoleUkeyBindingEntity roleUkeyBindingEntity = list.get(0);
|
||||
String publicKey = roleUkeyBindingEntity.getUkeyPubkey();
|
||||
try{
|
||||
verifySm2Signature(signValueSource, auditSign, publicKey);
|
||||
} catch (Exception e){
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
try{
|
||||
OperationAuditLogEntity updateEntity = new OperationAuditLogEntity();
|
||||
updateEntity.setLogId(existLog.getLogId());
|
||||
updateEntity.setAuditStatus(AuditStatus.REVIEWED);
|
||||
updateEntity.setAuditResult(req.getAuditResult());
|
||||
updateEntity.setAuditComment(trim(req.getAuditComment()));
|
||||
updateEntity.setAuditedBy(auditorUser);
|
||||
updateEntity.setAuditedAt(LocalDateTime.now());
|
||||
|
||||
operationAuditLogRepository.updateByLogId(updateEntity);
|
||||
} catch (Exception e){
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "日志审计更新失败");
|
||||
}
|
||||
|
||||
operationAuditLogRepository.updateByLogId(updateEntity);
|
||||
|
||||
log.info("日志复核完成, logId: {}, 审计人: {}, 结果: {}, {}", logId, auditorUser, req.getAuditResult(), req.getAuditComment());
|
||||
}
|
||||
|
||||
|
||||
private static String buildSignValueSource(OperationAuditLogEntity existLog, AuditResult auditResult) {
|
||||
String signValueSource = trim(existLog.getLogId()) +
|
||||
existLog.getOperatorRoleCode() +
|
||||
existLog.getOperatorAuthLevel() +
|
||||
existLog.getModuleCode() +
|
||||
existLog.getActionType() +
|
||||
trim(existLog.getRemoteIp()) +
|
||||
existLog.getOperationResult() +
|
||||
trim(existLog.getSummary()) +
|
||||
trim(existLog.getErrorMessage()) +
|
||||
existLog.getAuditStatus() +
|
||||
auditResult +
|
||||
trim(existLog.getSignValue()) +
|
||||
existLog.getOccurredAt();
|
||||
|
||||
return signValueSource;
|
||||
}
|
||||
|
||||
|
||||
public boolean verifySm2Signature(String data, String signatureBase64, String publicKeyBase64) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance("EC", PROVIDER);
|
||||
PublicKey publicKey = keyFactory.generatePublic(keySpec);
|
||||
|
||||
Signature signature = Signature.getInstance(SIGNATURE_ALGO, PROVIDER);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(data.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
|
||||
return signature.verify(signatureBytes);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("SM2验签失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeBlob(String value, String message) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
@@ -238,7 +291,7 @@ public class OperationAuditService {
|
||||
}
|
||||
|
||||
|
||||
private String trim(String s){
|
||||
private static String trim(String s){
|
||||
return s == null ? "" : s.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ spring:
|
||||
|
||||
server:
|
||||
# TMS HTTP 服务监听端口。
|
||||
port: 8080
|
||||
port: 8088
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
@@ -97,10 +97,10 @@ tms:
|
||||
standard-web-organization-json-path: ${TMS_INIT_EXECUTOR_STANDARD_WEB_ORGANIZATION_JSON_PATH:${tms.init.executor.standard-web-target-dir}/organization.json}
|
||||
# 标准版 CMSP/CMTP 启动用户;start_standard_apps.sh 会读取该值。
|
||||
standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i}
|
||||
# 标准版 CMSP/CMTP 启动命令。
|
||||
standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}
|
||||
# 标准版 CMSP/CMTP 停止命令。
|
||||
standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:/home/tms/bin/stop_standard_apps.sh}
|
||||
# 标准版 CMSP/CMTP 启动命令;默认通过 root 受控 wrapper 执行,确保停启能跨用户处理 cmep4i 进程。
|
||||
standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:sudo -n /usr/local/sbin/tms-start-standard-apps}
|
||||
# 标准版 CMSP/CMTP 停止命令;默认通过 root 受控 wrapper 执行,避免 tms 用户无权限 kill cmep4i 进程。
|
||||
standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:sudo -n /usr/local/sbin/tms-stop-standard-apps}
|
||||
# 标准版 nginx 启动命令。
|
||||
standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:sudo -n /usr/local/sbin/tms-start-standard-nginx}
|
||||
# 标准版 nginx 停止命令。
|
||||
@@ -135,9 +135,8 @@ tms:
|
||||
rabbit-admin-username: ${TMS_INIT_EXECUTOR_RABBIT_ADMIN_USERNAME:admin}
|
||||
# RabbitMQ 管理员密码(预制)。
|
||||
rabbit-admin-password: ${TMS_INIT_EXECUTOR_RABBIT_ADMIN_PASSWORD:admin}
|
||||
# RabbitMQ 控制命令(用于新增用户、授权等)。
|
||||
# 建议线上配置为绝对路径,例如 /usr/local/rabbitmq/sbin/rabbitmqctl。
|
||||
rabbitmq-ctl-command: ${TMS_INIT_EXECUTOR_RABBITMQ_CTL_COMMAND:/usr/local/rabbitmq/sbin/rabbitmqctl}
|
||||
# RabbitMQ 控制命令(用于新增用户、授权等);默认通过 root 受控 wrapper 执行,避免 tms 与 RabbitMQ 服务端 Erlang cookie 不一致。
|
||||
rabbitmq-ctl-command: ${TMS_INIT_EXECUTOR_RABBITMQ_CTL_COMMAND:sudo -n /usr/local/sbin/tms-rabbitmqctl}
|
||||
# RabbitMQ Admin CLI 命令路径(用于创建队列等)。
|
||||
rabbitmq-admin-command: ${TMS_INIT_EXECUTOR_RABBITMQ_ADMIN_COMMAND:/usr/sbin/rabbitmqadmin}
|
||||
# 标准版 RabbitMQ 初始化脚本路径(保留兼容;MQ_USER_APPLY 当前优先直接调用 rabbitmqctl/rabbitmqadmin)。
|
||||
@@ -322,7 +321,7 @@ tms:
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8080/actuator/health}
|
||||
health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8088/actuator/health}
|
||||
# 健康检查请求超时(秒)。
|
||||
health-check-timeout-seconds: ${TMS_BACKUP_HEALTH_CHECK_TIMEOUT_SECONDS:5}
|
||||
# 健康检查最大等待时间(秒);TMS 启动命令返回后,Spring Boot 仍可能需要一段时间才监听端口。
|
||||
@@ -343,7 +342,7 @@ tms:
|
||||
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
|
||||
internal-auth:
|
||||
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试角色。
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
|
||||
@@ -352,7 +351,7 @@ tms:
|
||||
debug-session-token: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_SESSION_TOKEN:DEBUG-BYPASS}
|
||||
replay:
|
||||
# 是否启用防重放校验;本地 Postman/联调可临时设为 false,生产环境应保持 true。
|
||||
enabled: ${TMS_SECURITY_REPLAY_ENABLED:false}
|
||||
enabled: ${TMS_SECURITY_REPLAY_ENABLED:true}
|
||||
openapi:
|
||||
# 外部签名服务接口允许的时间戳偏差(秒),防重放。
|
||||
timestamp-skew-seconds: 300
|
||||
|
||||
@@ -33,6 +33,7 @@ class ResourceBackupPropertiesTest {
|
||||
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_resource_restore_task"));
|
||||
Assertions.assertEquals(300, properties.getMysqldumpTimeoutSeconds());
|
||||
Assertions.assertEquals(300, properties.getRestoreDbTimeoutSeconds());
|
||||
Assertions.assertEquals("http://127.0.0.1:8088/actuator/health", properties.getHealthCheckUrl());
|
||||
Assertions.assertEquals(180, properties.getHealthCheckMaxWaitSeconds());
|
||||
Assertions.assertEquals(5, properties.getHealthCheckRetryIntervalSeconds());
|
||||
}
|
||||
|
||||
+44
@@ -332,6 +332,33 @@ class ApplyResourceBackupScriptContractTest {
|
||||
Assertions.assertTrue(status.contains("database restore failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteDatabaseSqlWithoutConnectingToTargetDatabaseFirst() throws Exception {
|
||||
Path commandLog = tempDir.resolve("mysql-restore.log");
|
||||
Path mysql = tempDir.resolve("mysql");
|
||||
Files.writeString(mysql, """
|
||||
#!/usr/bin/env bash
|
||||
for arg in "$@"; do
|
||||
printf 'ARG:%%s\\n' "$arg" >> "%s"
|
||||
done
|
||||
input="$(cat)"
|
||||
if [ -n "$input" ]; then
|
||||
printf 'STDIN:%%s\\n' "$input" >> "%s"
|
||||
fi
|
||||
""".formatted(commandLog, commandLog));
|
||||
mysql.toFile().setExecutable(true);
|
||||
Path sqlFile = tempDir.resolve("CMEP.sql");
|
||||
Files.writeString(sqlFile, "CREATE DATABASE IF NOT EXISTS `CMEP`;\nUSE `CMEP`;\nselect 1;");
|
||||
|
||||
int exitCode = runDbRestoreScriptForExit(mysql, "CMEP", sqlFile);
|
||||
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("ARG:--host=127.0.0.1"));
|
||||
Assertions.assertFalse(log.contains("ARG:CMEP" + System.lineSeparator()));
|
||||
Assertions.assertTrue(log.contains("STDIN:CREATE DATABASE IF NOT EXISTS `CMEP`;"));
|
||||
}
|
||||
|
||||
private static void runScript(Path workDir, Map<String, String> environment) throws Exception {
|
||||
int exitCode = runScriptForExit(workDir, environment);
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
@@ -350,6 +377,23 @@ class ApplyResourceBackupScriptContractTest {
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
private static int runDbRestoreScriptForExit(Path mysqlPath, String dbName, Path sqlFile) throws Exception {
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
Path.of("scripts/resource-restore/restore-db.sh").toAbsolutePath().normalize().toString(),
|
||||
"--mysql-path", mysqlPath.toString(),
|
||||
"--host", "127.0.0.1",
|
||||
"--port", "4000",
|
||||
"--username", "root",
|
||||
"--password", "secret",
|
||||
"--db-name", dbName,
|
||||
"--sql-file", sqlFile.toString()
|
||||
);
|
||||
builder.directory(Path.of(".").toAbsolutePath().normalize().toFile());
|
||||
Process process = builder.start();
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
private Path fakeScript(String name, Path commandLog) throws Exception {
|
||||
Path script = tempDir.resolve(name);
|
||||
Files.writeString(script, """
|
||||
|
||||
@@ -39,6 +39,36 @@ class MqReplayScriptContractTest {
|
||||
Assertions.assertTrue(log.contains("set_permissions -p /RQ tms .* .* .*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReplayRabbitMqThroughPrefixedRootWrapperCommand() throws Exception {
|
||||
Path context = tempDir.resolve("mq-restore-context.json");
|
||||
Files.writeString(context, """
|
||||
{"schemaVersion":"1.0","productType":"ENTERPRISE","mqType":"RABBITMQ","orgCode":"BKCHCNBJXXX",
|
||||
"mq":{"channelUsername":"bank_user","channelPassword":"Secret123!"}}
|
||||
""");
|
||||
Path commandLog = tempDir.resolve("commands.log");
|
||||
Path sudo = fakeSudo(commandLog);
|
||||
Path wrapper = tempDir.resolve("tms-rabbitmqctl");
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
Path.of("scripts/resource-restore/replay-mq.sh").toAbsolutePath().normalize().toString(),
|
||||
"--context-file",
|
||||
context.toString(),
|
||||
"--work-dir",
|
||||
tempDir.resolve("work").toString()
|
||||
);
|
||||
builder.environment().put("RABBITMQ_CTL_COMMAND", sudo + " -n " + wrapper);
|
||||
Process process = builder.start();
|
||||
|
||||
Assertions.assertEquals(0, process.waitFor(), new String(process.getErrorStream().readAllBytes()));
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("-n " + wrapper + " status"));
|
||||
Assertions.assertTrue(log.contains("-n " + wrapper + " add_vhost /RQ"));
|
||||
Assertions.assertTrue(log.contains("-n " + wrapper + " add_user bank_user Secret123!"));
|
||||
Assertions.assertTrue(log.contains("-n " + wrapper + " set_permissions -p /RQ bank_user .* .* .*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateToCustomReplayCommandWhenConfigured() throws Exception {
|
||||
Path context = tempDir.resolve("mq-restore-context.json");
|
||||
@@ -71,4 +101,15 @@ class MqReplayScriptContractTest {
|
||||
script.toFile().setExecutable(true);
|
||||
return script;
|
||||
}
|
||||
|
||||
private Path fakeSudo(Path commandLog) throws Exception {
|
||||
Path script = tempDir.resolve("sudo");
|
||||
Files.writeString(script, """
|
||||
#!/usr/bin/env bash
|
||||
printf '%%s\\n' "$*" >> "%s"
|
||||
exit 0
|
||||
""".formatted(commandLog));
|
||||
script.toFile().setExecutable(true);
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
+77
-6
@@ -1031,6 +1031,8 @@ class ConfigurableInitStepExecutorTest {
|
||||
Path stopRabbitScript = scriptDir.resolve("stop_standard_rabbitmq.sh");
|
||||
Path checkScript = scriptDir.resolve("check_standard_runtime.sh");
|
||||
Path vendorStepRunner = scriptDir.resolve("root-sbin/tms-run-standard-vendor-step");
|
||||
Path startAppsRunner = scriptDir.resolve("root-sbin/tms-start-standard-apps");
|
||||
Path stopAppsRunner = scriptDir.resolve("root-sbin/tms-stop-standard-apps");
|
||||
|
||||
Assertions.assertTrue(Files.exists(dbScript));
|
||||
Assertions.assertTrue(Files.exists(appScript));
|
||||
@@ -1040,6 +1042,8 @@ class ConfigurableInitStepExecutorTest {
|
||||
Assertions.assertTrue(Files.exists(stopRabbitScript));
|
||||
Assertions.assertTrue(Files.exists(checkScript));
|
||||
Assertions.assertTrue(Files.exists(vendorStepRunner));
|
||||
Assertions.assertTrue(Files.exists(startAppsRunner));
|
||||
Assertions.assertTrue(Files.exists(stopAppsRunner));
|
||||
|
||||
String dbContent = Files.readString(dbScript);
|
||||
Assertions.assertTrue(dbContent.contains("SCHEMA-DDL.sql"));
|
||||
@@ -1079,6 +1083,16 @@ class ConfigurableInitStepExecutorTest {
|
||||
Assertions.assertTrue(vendorRunnerContent.contains("setftq"));
|
||||
Assertions.assertTrue(vendorRunnerContent.contains("settlq"));
|
||||
Assertions.assertTrue(vendorRunnerContent.contains("setsptp"));
|
||||
|
||||
String startAppsRunnerContent = Files.readString(startAppsRunner);
|
||||
Assertions.assertTrue(startAppsRunnerContent.contains("start_standard_apps.sh"));
|
||||
Assertions.assertTrue(startAppsRunnerContent.contains("exec"));
|
||||
|
||||
String stopAppsRunnerContent = Files.readString(stopAppsRunner);
|
||||
Assertions.assertTrue(stopAppsRunnerContent.contains("stop_standard_apps.sh"));
|
||||
Assertions.assertTrue(stopAppsRunnerContent.contains("CMEP-CMSP"));
|
||||
Assertions.assertTrue(stopAppsRunnerContent.contains("CMEP-CMTP"));
|
||||
Assertions.assertTrue(stopAppsRunnerContent.contains("exit 1"));
|
||||
Assertions.assertTrue(vendorRunnerContent.contains("runuser -u"));
|
||||
|
||||
String stopRabbitContent = Files.readString(stopRabbitScript);
|
||||
@@ -1107,18 +1121,20 @@ class ConfigurableInitStepExecutorTest {
|
||||
String exampleContent = Files.readString(exampleConfig);
|
||||
Assertions.assertTrue(exampleContent.contains("standard-db-apply-command: /home/tms/bin/apply_standard_db.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-run-user: cmep4i"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-app-start-command: /home/tms/bin/start_standard_apps.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-app-stop-command: /home/tms/bin/stop_standard_apps.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-app-start-command: sudo -n /usr/local/sbin/tms-start-standard-apps"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-app-stop-command: sudo -n /usr/local/sbin/tms-stop-standard-apps"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-nginx-stop-command: /home/tms/bin/stop_standard_nginx.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("standard-post-check-command: /home/tms/bin/check_standard_runtime.sh"));
|
||||
Assertions.assertTrue(exampleContent.contains("rabbitmq-ctl-command: sudo -n /usr/local/sbin/tms-rabbitmqctl"));
|
||||
|
||||
String applicationContent = Files.readString(applicationConfig);
|
||||
Assertions.assertTrue(applicationContent.contains("standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:/home/tms/bin/stop_standard_apps.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-nginx-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_STOP_COMMAND:/home/tms/bin/stop_standard_nginx.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:sudo -n /usr/local/sbin/tms-start-standard-apps}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:sudo -n /usr/local/sbin/tms-stop-standard-apps}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:sudo -n /usr/local/sbin/tms-start-standard-nginx}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-nginx-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_STOP_COMMAND:sudo -n /usr/local/sbin/tms-stop-standard-nginx}"));
|
||||
Assertions.assertTrue(applicationContent.contains("standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}"));
|
||||
Assertions.assertTrue(applicationContent.contains("rabbitmq-ctl-command: ${TMS_INIT_EXECUTOR_RABBITMQ_CTL_COMMAND:sudo -n /usr/local/sbin/tms-rabbitmqctl}"));
|
||||
|
||||
String readmeContent = Files.readString(readme);
|
||||
Assertions.assertTrue(readmeContent.contains("scripts/standard-init/"));
|
||||
@@ -1247,6 +1263,61 @@ class ConfigurableInitStepExecutorTest {
|
||||
Assertions.assertTrue(callsContent.contains("set_permissions -p /RQ bank_user .* .* .*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyStandardRabbitUserThroughPrefixedRootWrapperCommand() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-wrapper-test");
|
||||
Path cpconfig = tempRoot.resolve("cpconfig.cfg");
|
||||
Path calls = tempRoot.resolve("calls.log");
|
||||
Path sudo = tempRoot.resolve("sudo");
|
||||
Path wrapper = tempRoot.resolve("tms-rabbitmqctl");
|
||||
Files.writeString(cpconfig, "MY_CIPSID_OR_BIC=AAAABBBBXXX\n");
|
||||
Files.writeString(
|
||||
sudo,
|
||||
"#!/bin/bash\n"
|
||||
+ "echo \"$*\" >> \"" + calls + "\"\n"
|
||||
+ "exit 0\n"
|
||||
);
|
||||
Assertions.assertTrue(sudo.toFile().setExecutable(true));
|
||||
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setMode(InitExecutorProperties.Mode.LOCAL);
|
||||
properties.setLogDir(tempRoot.resolve("logs").toString());
|
||||
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
|
||||
properties.setStandardCpconfigPath(cpconfig.toString());
|
||||
properties.setRabbitmqCtlCommand(sudo + " -n " + wrapper);
|
||||
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
|
||||
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setTaskId("TASK-RABBIT-WRAPPER-1");
|
||||
task.setProductType("ENTERPRISE");
|
||||
task.setInitPlanJson(buildInitPlanJson(
|
||||
"ENTERPRISE",
|
||||
Map.of(
|
||||
"orgCodeType", "BIC",
|
||||
"orgCode", "AAAABBBBXXX",
|
||||
"orgNameCn", "测试机构",
|
||||
"deployMode", "SINGLE",
|
||||
"nodes", Map.of("node01Ip", "10.0.0.1"),
|
||||
"mq", Map.of("mqType", "RABBITMQ", "channelUsername", "bank_user", "channelPassword", "bank_pass"),
|
||||
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
|
||||
)
|
||||
));
|
||||
|
||||
InitTaskStepEntity step = new InitTaskStepEntity();
|
||||
step.setStepNo(1);
|
||||
step.setStepCode("MQ_USER_APPLY");
|
||||
step.setCommandLine("internal::mq_user_apply");
|
||||
|
||||
InitStepExecutionResult result = executor.execute(task, step);
|
||||
|
||||
Assertions.assertTrue(result.isSuccess());
|
||||
String callsContent = Files.readString(calls);
|
||||
Assertions.assertTrue(callsContent.contains("-n " + wrapper + " status"));
|
||||
Assertions.assertTrue(callsContent.contains("-n " + wrapper + " add_vhost /RQ"));
|
||||
Assertions.assertTrue(callsContent.contains("-n " + wrapper + " add_user bank_user bank_pass"));
|
||||
Assertions.assertTrue(callsContent.contains("-n " + wrapper + " set_permissions -p /RQ bank_user .* .* .*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyStandardRabbitUserForIndirectRabbitmqTlq() throws IOException {
|
||||
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-setup-indirect-tlq-test");
|
||||
|
||||
Reference in New Issue
Block a user