fix:资源备份和恢复

This commit is contained in:
waner
2026-05-06 13:52:24 +08:00
parent 325d7ae42d
commit 8287289167
17 changed files with 387 additions and 11 deletions
@@ -4,6 +4,7 @@ import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.util.HttpResponseUtil;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.log.service.OperationAuditService;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -36,7 +37,7 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
RequireInternalAuth requireInternalAuth = findAnnotation(handlerMethod, RequireInternalAuth.class);
if (requireInternalAuth != null) {
String currentRole = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
if (!requireInternalAuth.role().getCode().equals(currentRole)) {
if (!roleAllowed(currentRole, requireInternalAuth)) {
writeForbidden(response, "角色无权限访问");
operationAuditService.recordDeniedLog(request, handlerMethod, "角色无权限访问");
return false;
@@ -52,6 +53,22 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
return true;
}
private boolean roleAllowed(String currentRole, RequireInternalAuth requireInternalAuth) {
if (currentRole == null || currentRole.isBlank()) {
return false;
}
RoleCode[] anyRole = requireInternalAuth.anyRole();
if (anyRole.length == 0) {
return requireInternalAuth.role().getCode().equals(currentRole);
}
for (RoleCode roleCode : anyRole) {
if (roleCode.getCode().equals(currentRole)) {
return true;
}
}
return false;
}
private boolean authLevelAllowed(String currentAuthLevel, AuthLevel requiredAuthLevel) {
if (currentAuthLevel == null) {
return false;
@@ -11,7 +11,9 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireInternalAuth {
RoleCode role();
RoleCode role() default RoleCode.SUPER_ADMIN;
RoleCode[] anyRole() default {};
AuthLevel authLevel();
}
@@ -1,6 +1,9 @@
package com.cisd.tms.modules.backup.controller;
import com.cisd.tms.common.api.ApiResponse;
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.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
@@ -8,6 +11,9 @@ import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.service.ResourceBackupService;
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
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.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -40,12 +46,15 @@ public class ResourceBackupController {
@PostMapping
@ReplayProtected
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN}, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.BACKUP, summary = "创建资源备份任务")
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
return ApiResponse.success(resourceBackupService.createBackupTask(request));
}
@GetMapping("/{taskId}")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源备份任务详情", description = "返回资源备份任务详情。")
public ApiResponse<ResourceBackupTaskDetailResponse> taskDetail(
@Parameter(description = "备份任务号")
@@ -55,6 +64,8 @@ public class ResourceBackupController {
}
@GetMapping("/{taskId}/download")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN}, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.EXPORT, summary = "下载资源备份包")
@Operation(summary = "下载资源备份包", description = "以浏览器附件方式下载资源备份包。")
public ResponseEntity<Resource> downloadPackage(
@Parameter(description = "备份任务号")
@@ -70,6 +81,7 @@ public class ResourceBackupController {
}
@GetMapping("/{taskId}/steps")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源备份任务步骤", description = "返回资源备份任务步骤列表。")
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
@Parameter(description = "备份任务号")
@@ -79,6 +91,7 @@ public class ResourceBackupController {
}
@GetMapping("/{taskId}/steps/{stepNo}/log")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源备份步骤日志", description = "返回资源备份任务步骤日志。")
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
@Parameter(description = "备份任务号")
@@ -1,6 +1,9 @@
package com.cisd.tms.modules.backup.controller;
import com.cisd.tms.common.api.ApiResponse;
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.backup.dto.request.CreateResourceRestoreRequest;
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
@@ -11,6 +14,9 @@ import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
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.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -37,6 +43,8 @@ public class ResourceRestoreController {
private final ResourceTaskQueryService resourceTaskQueryService;
@PostMapping("/precheck")
// @RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.ACCESS, summary = "预检资源恢复包")
@Operation(summary = "预检资源恢复请求", description = "根据 fileId 校验备份包头、签名和恢复范围摘要。")
public ApiResponse<ResourceRestorePrecheckResponse> precheck(@Valid @RequestBody ResourceRestorePrecheckRequest request) {
return ApiResponse.success(resourcePrecheckService.precheck(request));
@@ -44,12 +52,15 @@ public class ResourceRestoreController {
@PostMapping
@ReplayProtected
// @RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.RECOVER, summary = "创建资源恢复任务")
@Operation(summary = "创建资源恢复任务", description = "根据预检号创建资源恢复任务。")
public ApiResponse<CreateResourceRestoreResponse> createRestoreTask(@Valid @RequestBody CreateResourceRestoreRequest request) {
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
}
@GetMapping("/{taskId}")
// @RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
@Parameter(description = "恢复任务号")
@@ -59,6 +70,7 @@ public class ResourceRestoreController {
}
@GetMapping("/{taskId}/steps")
// @RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
@Parameter(description = "恢复任务号")
@@ -68,6 +80,7 @@ public class ResourceRestoreController {
}
@GetMapping("/{taskId}/steps/{stepNo}/log")
// @RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源恢复步骤日志", description = "返回资源恢复步骤日志。")
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
@Parameter(description = "恢复任务号")
@@ -1,11 +1,16 @@
package com.cisd.tms.modules.backup.repository;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import java.util.List;
import java.util.Optional;
public interface ResourceBackupTaskRepository {
void save(ResourceBackupTaskEntity entity);
void update(ResourceBackupTaskEntity entity);
Optional<ResourceBackupTaskEntity> findByTaskId(String taskId);
List<ResourceBackupTaskEntity> findAll();
}
@@ -3,6 +3,7 @@ package com.cisd.tms.modules.backup.repository.impl;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@@ -20,8 +21,19 @@ public class ResourceBackupTaskRepositoryImpl implements ResourceBackupTaskRepos
resourceBackupTaskMapper.insert(entity);
}
@Override
public void update(ResourceBackupTaskEntity entity) {
resourceBackupTaskMapper.updateById(entity);
}
@Override
public Optional<ResourceBackupTaskEntity> findByTaskId(String taskId) {
return Optional.ofNullable(resourceBackupTaskMapper.selectByTaskId(taskId));
}
@Override
public List<ResourceBackupTaskEntity> findAll() {
List<ResourceBackupTaskEntity> tasks = resourceBackupTaskMapper.selectList(null);
return tasks == null ? List.of() : tasks;
}
}
@@ -40,6 +40,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
@Override
public ResourceKeysetStatusResponse getKeysetStatus() {
@@ -57,6 +58,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
@Override
public CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request) {
resourceTaskAdmissionGuard.assertCanStartBackup();
// 资源备份必须基于“最近一次成功初始化”快照收集上下文,避免前端重新传一套可能失真的参数。
InitTaskEntity initTask = initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task is missing"));
@@ -77,6 +79,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
entity.setCreateTime(now);
entity.setUpdateTime(now);
entity.setStartTime(now);
resourceBackupTaskRepository.save(entity);
try {
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
@@ -106,7 +109,8 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
entity.setErrorMessage(ex.getMessage());
}
entity.setFinishTime(LocalDateTime.now());
resourceBackupTaskRepository.save(entity);
entity.setUpdateTime(entity.getFinishTime());
resourceBackupTaskRepository.update(entity);
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
response.setTaskId(taskId);
@@ -36,10 +36,12 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
private final ResourceBackupProperties resourceBackupProperties;
private final ResourcePayloadExtractor resourcePayloadExtractor;
private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher;
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
@Override
public CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request) {
resourceTaskAdmissionGuard.assertCanStartRestore();
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "必须确认恢复风险");
}
@@ -0,0 +1,106 @@
package com.cisd.tms.modules.backup.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class ResourceTaskAdmissionGuard {
private static final Set<String> ACTIVE_BACKUP_STATUSES = Set.of("RUNNING");
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("APPLYING", "RUNNING", "HANDOFF", "PENDING");
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final InitTaskRepository initTaskRepository;
private final UpgradeTaskRepository upgradeTaskRepository;
private final RestoreStatusFileReader restoreStatusFileReader;
public ResourceTaskAdmissionGuard(
ResourceBackupTaskRepository resourceBackupTaskRepository,
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
InitTaskRepository initTaskRepository,
UpgradeTaskRepository upgradeTaskRepository,
RestoreStatusFileReader restoreStatusFileReader
) {
this.resourceBackupTaskRepository = resourceBackupTaskRepository;
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
this.initTaskRepository = initTaskRepository;
this.upgradeTaskRepository = upgradeTaskRepository;
this.restoreStatusFileReader = restoreStatusFileReader;
}
public synchronized void assertCanStartBackup() {
assertNoActiveBackup();
assertNoActiveRestore();
assertNoRunningInitOrReset();
assertNoRunningUpgrade();
}
public synchronized void assertCanStartRestore() {
assertNoActiveBackup();
assertNoActiveRestore();
assertNoRunningInitOrReset();
assertNoRunningUpgrade();
}
private void assertNoActiveBackup() {
for (ResourceBackupTaskEntity task : safeList(resourceBackupTaskRepository.findAll())) {
if (ACTIVE_BACKUP_STATUSES.contains(trim(task.getStatus()))) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "资源备份任务正在执行:" + trim(task.getTaskId()));
}
}
}
private void assertNoActiveRestore() {
for (ResourceRestoreTaskEntity task : safeList(resourceRestoreTaskRepository.findAll())) {
if (!ACTIVE_RESTORE_STATUSES.contains(trim(task.getStatus()))) {
continue;
}
RestoreStatusFileReader.RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(task));
if ("SUCCESS".equalsIgnoreCase(trim(snapshot.status())) || "FAILED".equalsIgnoreCase(trim(snapshot.status()))) {
continue;
}
throw new BizException(ErrorCode.CONFLICT.getCode(), "资源恢复任务正在执行:" + trim(task.getTaskId()));
}
}
private void assertNoRunningInitOrReset() {
initTaskRepository.findLatestByStatus("RUNNING").ifPresent(task -> {
throw new BizException(ErrorCode.CONFLICT.getCode(), "初始化或重置任务正在执行:" + trim(task.getTaskId()));
});
}
private void assertNoRunningUpgrade() {
upgradeTaskRepository.findRunningTask().ifPresent(task -> {
throw new BizException(ErrorCode.CONFLICT.getCode(), "升级任务正在执行:" + trim(task.getTaskId()));
});
}
private static Path resolveTaskDir(ResourceRestoreTaskEntity task) {
String runnerStatePath = trim(task.getRunnerStatePath());
if (runnerStatePath.isEmpty()) {
return null;
}
Path statePath = Path.of(runnerStatePath).normalize();
return statePath.getParent();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private static <T> List<T> safeList(List<T> items) {
return items == null ? List.of() : items;
}
}
@@ -1,5 +1,5 @@
package com.cisd.tms.modules.log.enums;
public enum ModuleCode {
AUTH, INIT, UPGRADE, DEVICE, NETWORK, KEY, SYSTEM, SECURITY, LOG
AUTH, INIT, UPGRADE, DEVICE, NETWORK, KEY, SYSTEM, SECURITY, LOG, BACKUP
}