fix:权限调整

This commit is contained in:
waner
2026-05-19 17:12:53 +08:00
parent a30c029fc4
commit 58dc015e01
14 changed files with 651 additions and 39 deletions
@@ -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 {
+6 -7
View File
@@ -13,7 +13,7 @@ spring:
server:
# TMS HTTP 服务监听端口。
port: 8080
port: 8088
management:
endpoints:
@@ -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 仍可能需要一段时间才监听端口。
@@ -340,7 +339,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}
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
@@ -349,7 +348,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
@@ -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;
}
}
@@ -1112,13 +1112,15 @@ class ConfigurableInitStepExecutorTest {
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-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 +1249,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");