fix:资源备份和恢复
This commit is contained in:
parent
325d7ae42d
commit
8287289167
@ -276,6 +276,7 @@ Open:
|
||||
- Java 会生成 shell 友好的 `restore-files.tsv`、`restore-databases.tsv`、`restore-mq.tsv`
|
||||
- 创建恢复任务后启动 `apply-resource-backup.sh --work-dir <workDir>`,由 shell 执行停服务、文件覆盖、SQL 导入、MQ replay、起服务和健康检查
|
||||
- shell 侧状态写入 `status.json`,日志写入 `restore.log`
|
||||
- 第一版资源恢复是停机恢复。创建恢复任务后当前TMS服务可能中断,前端应提示用户恢复期间页面/API可能不可用,恢复结果以后续健康检查、重新登录和 `status.json` / `restore.log` 为准。
|
||||
- `FILE` 回写 manifest 明确声明的配置/License 文件
|
||||
- `DATABASE` 恢复:
|
||||
- `db/TMS.sql`
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -79,6 +79,21 @@ class InternalAuthorizationInterceptorTest {
|
||||
Mockito.verifyNoInteractions(operationAuditService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowAnyConfiguredRoleWhenEndpointDeclaresRoleSet() throws Exception {
|
||||
OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class);
|
||||
InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/resource-backups");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SUPER_ADMIN");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean allowed = interceptor.preHandle(request, response, handler("opsOrSuperAdminFullEndpoint"));
|
||||
|
||||
Assertions.assertTrue(allowed);
|
||||
Mockito.verifyNoInteractions(operationAuditService);
|
||||
}
|
||||
|
||||
private static HandlerMethod handler(String methodName) throws NoSuchMethodException {
|
||||
DemoController controller = new DemoController();
|
||||
return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName));
|
||||
@ -94,5 +109,15 @@ class InternalAuthorizationInterceptorTest {
|
||||
@RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED)
|
||||
public void keyAdminLimitedEndpoint() {
|
||||
}
|
||||
|
||||
@RequireInternalAuth(
|
||||
anyRole = {
|
||||
com.cisd.tms.modules.auth.enums.RoleCode.OPS_ADMIN,
|
||||
com.cisd.tms.modules.auth.enums.RoleCode.SUPER_ADMIN
|
||||
},
|
||||
authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL
|
||||
)
|
||||
public void opsOrSuperAdminFullEndpoint() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,4 +17,12 @@ class ResourceBackupEndToEndContractTest {
|
||||
Assertions.assertTrue(restoreController.contains("/precheck"));
|
||||
Assertions.assertTrue(restoreController.contains("/steps/{stepNo}/log"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readmeShouldDeclareRestoreIsAColdStopOperation() throws Exception {
|
||||
String readme = Files.readString(Path.of("/Users/waner/Work/CISD/文档/tms-framework/README.md"));
|
||||
|
||||
Assertions.assertTrue(readme.contains("停机恢复"));
|
||||
Assertions.assertTrue(readme.contains("创建恢复任务后当前TMS服务可能中断"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
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.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ResourceBackupAuthorizationAuditContractTest {
|
||||
|
||||
@Test
|
||||
void shouldRequireOpsOrSuperAdminFullForBackupAndRestoreEndpoints() throws Exception {
|
||||
assertFullOpsOrSuperAdmin(ResourceBackupController.class.getMethod(
|
||||
"createBackupTask",
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest.class
|
||||
));
|
||||
assertFullOpsOrSuperAdmin(ResourceBackupController.class.getMethod("downloadPackage", String.class));
|
||||
assertFullOpsOrSuperAdmin(ResourceRestoreController.class.getMethod(
|
||||
"precheck",
|
||||
com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest.class
|
||||
));
|
||||
assertFullOpsOrSuperAdmin(ResourceRestoreController.class.getMethod(
|
||||
"createRestoreTask",
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.class
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAuditBackupDownloadPrecheckAndRestoreCreation() throws Exception {
|
||||
assertAudit(ResourceBackupController.class.getMethod(
|
||||
"createBackupTask",
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest.class
|
||||
), ActionType.BACKUP);
|
||||
assertAudit(ResourceBackupController.class.getMethod("downloadPackage", String.class), ActionType.EXPORT);
|
||||
assertAudit(ResourceRestoreController.class.getMethod(
|
||||
"precheck",
|
||||
com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest.class
|
||||
), ActionType.ACCESS);
|
||||
assertAudit(ResourceRestoreController.class.getMethod(
|
||||
"createRestoreTask",
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.class
|
||||
), ActionType.RECOVER);
|
||||
}
|
||||
|
||||
private static void assertFullOpsOrSuperAdmin(Method method) {
|
||||
RequireInternalAuth auth = method.getAnnotation(RequireInternalAuth.class);
|
||||
Assertions.assertNotNull(auth, method.getName() + " should require internal auth");
|
||||
Assertions.assertEquals(AuthLevel.FULL, auth.authLevel());
|
||||
Assertions.assertArrayEquals(new RoleCode[]{RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, auth.anyRole());
|
||||
}
|
||||
|
||||
private static void assertAudit(Method method, ActionType actionType) {
|
||||
AuditedOperation audit = method.getAnnotation(AuditedOperation.class);
|
||||
Assertions.assertNotNull(audit, method.getName() + " should be audited");
|
||||
Assertions.assertEquals(ModuleCode.BACKUP, audit.module());
|
||||
Assertions.assertEquals(actionType, audit.action());
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceBackupServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskAdmissionGuard;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
@ -29,6 +30,7 @@ class ResourceBackupServiceTest {
|
||||
ResourceDiscoveryService resourceDiscoveryService = Mockito.mock(ResourceDiscoveryService.class);
|
||||
ResourcePackageService resourcePackageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
|
||||
InitTaskEntity initTask = sampleInitTask();
|
||||
List<DiscoveredResourceItem> discoveredItems = List.of(resource("TMS_CONFIG", "/home/tms/config", true));
|
||||
@ -57,7 +59,8 @@ class ResourceBackupServiceTest {
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties()
|
||||
new ResourceBackupProperties(),
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
CreateResourceBackupResponse response = service.createBackupTask(sampleRequest());
|
||||
@ -68,7 +71,7 @@ class ResourceBackupServiceTest {
|
||||
Assertions.assertNotNull(response.getCreatedAt());
|
||||
|
||||
ArgumentCaptor<ResourceBackupTaskEntity> captor = ArgumentCaptor.forClass(ResourceBackupTaskEntity.class);
|
||||
Mockito.verify(resourceBackupTaskRepository).save(captor.capture());
|
||||
Mockito.verify(resourceBackupTaskRepository).update(captor.capture());
|
||||
ResourceBackupTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals("SUCCESS", saved.getStatus());
|
||||
Assertions.assertEquals(packageResult.getPackagePath(), saved.getPackagePath());
|
||||
@ -83,6 +86,7 @@ class ResourceBackupServiceTest {
|
||||
ResourceDiscoveryService resourceDiscoveryService = Mockito.mock(ResourceDiscoveryService.class);
|
||||
ResourcePackageService resourcePackageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
|
||||
InitTaskEntity initTask = sampleInitTask();
|
||||
List<DiscoveredResourceItem> discoveredItems = List.of(resource("TMS_CONFIG", "/home/tms/config", true));
|
||||
@ -103,7 +107,8 @@ class ResourceBackupServiceTest {
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties()
|
||||
new ResourceBackupProperties(),
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
CreateResourceBackupResponse response = service.createBackupTask(sampleRequest());
|
||||
@ -111,7 +116,7 @@ class ResourceBackupServiceTest {
|
||||
Assertions.assertEquals("FAILED", response.getStatus());
|
||||
|
||||
ArgumentCaptor<ResourceBackupTaskEntity> captor = ArgumentCaptor.forClass(ResourceBackupTaskEntity.class);
|
||||
Mockito.verify(resourceBackupTaskRepository).save(captor.capture());
|
||||
Mockito.verify(resourceBackupTaskRepository).update(captor.capture());
|
||||
ResourceBackupTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals("FAILED", saved.getStatus());
|
||||
Assertions.assertEquals("REQUIRED_RESOURCE_MISSING", saved.getErrorCode());
|
||||
|
||||
@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskAdmissionGuard;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.support.file.FileSystemResourcePrecheckStateStore;
|
||||
@ -40,6 +41,7 @@ class ResourceRestoreHandoffServiceTest {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
Files.createDirectories(packagePath.getParent());
|
||||
Files.writeString(packagePath, "package-bytes");
|
||||
@ -68,7 +70,8 @@ class ResourceRestoreHandoffServiceTest {
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
@ -103,6 +106,7 @@ class ResourceRestoreHandoffServiceTest {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse();
|
||||
precheckResponse.setCompatible(false);
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
@ -113,7 +117,8 @@ class ResourceRestoreHandoffServiceTest {
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
@ -133,6 +138,7 @@ class ResourceRestoreHandoffServiceTest {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse();
|
||||
precheckResponse.setExpiresAt(LocalDateTime.now().minusMinutes(1).toString());
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
@ -143,7 +149,8 @@ class ResourceRestoreHandoffServiceTest {
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
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.backup.service.impl.ResourceTaskAdmissionGuard;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity;
|
||||
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
class ResourceTaskAdmissionGuardTest {
|
||||
|
||||
@Test
|
||||
void shouldRejectBackupWhenAnotherBackupIsRunning() {
|
||||
ResourceBackupTaskRepository backupRepository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
Mockito.when(backupRepository.findAll()).thenReturn(List.of(activeBackup()));
|
||||
ResourceTaskAdmissionGuard guard = guard(backupRepository);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, guard::assertCanStartBackup);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("资源备份任务正在执行"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRestoreWhenInitIsRunning() {
|
||||
InitTaskRepository initTaskRepository = Mockito.mock(InitTaskRepository.class);
|
||||
InitTaskEntity running = new InitTaskEntity();
|
||||
running.setTaskId("INIT-001");
|
||||
running.setTaskType("INIT");
|
||||
running.setStatus("RUNNING");
|
||||
Mockito.when(initTaskRepository.findLatestByStatus("RUNNING")).thenReturn(Optional.of(running));
|
||||
ResourceTaskAdmissionGuard guard = guard(Mockito.mock(ResourceBackupTaskRepository.class), initTaskRepository);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, guard::assertCanStartRestore);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("初始化或重置任务正在执行"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRestoreWhenUpgradeIsRunning() {
|
||||
UpgradeTaskRepository upgradeTaskRepository = Mockito.mock(UpgradeTaskRepository.class);
|
||||
UpgradeTaskEntity running = new UpgradeTaskEntity();
|
||||
running.setTaskId("UPG-001");
|
||||
running.setStatus("RUNNING");
|
||||
Mockito.when(upgradeTaskRepository.findRunningTask()).thenReturn(Optional.of(running));
|
||||
ResourceTaskAdmissionGuard guard = guard(
|
||||
Mockito.mock(ResourceBackupTaskRepository.class),
|
||||
Mockito.mock(InitTaskRepository.class),
|
||||
upgradeTaskRepository
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, guard::assertCanStartRestore);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("升级任务正在执行"));
|
||||
}
|
||||
|
||||
private static ResourceBackupTaskEntity activeBackup() {
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
entity.setTaskId("RBKP-TASK-001");
|
||||
entity.setStatus("RUNNING");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static ResourceTaskAdmissionGuard guard(ResourceBackupTaskRepository backupRepository) {
|
||||
return guard(backupRepository, Mockito.mock(InitTaskRepository.class));
|
||||
}
|
||||
|
||||
private static ResourceTaskAdmissionGuard guard(ResourceBackupTaskRepository backupRepository, InitTaskRepository initTaskRepository) {
|
||||
return guard(backupRepository, initTaskRepository, Mockito.mock(UpgradeTaskRepository.class));
|
||||
}
|
||||
|
||||
private static ResourceTaskAdmissionGuard guard(
|
||||
ResourceBackupTaskRepository backupRepository,
|
||||
InitTaskRepository initTaskRepository,
|
||||
UpgradeTaskRepository upgradeTaskRepository
|
||||
) {
|
||||
return new ResourceTaskAdmissionGuard(
|
||||
backupRepository,
|
||||
Mockito.mock(ResourceRestoreTaskRepository.class),
|
||||
initTaskRepository,
|
||||
upgradeTaskRepository,
|
||||
Mockito.mock(RestoreStatusFileReader.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user