feat(audit): audit deploy/stop/promote with DEPLOYMENT category
Wires AuditService and AppVersionRepository into DeploymentController. Replaces null createdBy placeholder with currentUserId() on createDeployment/promote. Adds audit log entries (SUCCESS + FAILURE) for deploy_app, stop_deployment, and promote_deployment actions. Fixes FK violations in affected ITs by seeding the test-operator and alice users into the users table before deploy calls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,13 @@ package com.cameleer.server.app.controller;
|
||||
|
||||
import com.cameleer.server.app.runtime.DeploymentExecutor;
|
||||
import com.cameleer.server.app.web.EnvPath;
|
||||
import com.cameleer.server.core.admin.AuditCategory;
|
||||
import com.cameleer.server.core.admin.AuditResult;
|
||||
import com.cameleer.server.core.admin.AuditService;
|
||||
import com.cameleer.server.core.runtime.App;
|
||||
import com.cameleer.server.core.runtime.AppService;
|
||||
import com.cameleer.server.core.runtime.AppVersion;
|
||||
import com.cameleer.server.core.runtime.AppVersionRepository;
|
||||
import com.cameleer.server.core.runtime.Deployment;
|
||||
import com.cameleer.server.core.runtime.DeploymentService;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
@@ -12,14 +17,18 @@ import com.cameleer.server.core.runtime.RuntimeOrchestrator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -42,17 +51,23 @@ public class DeploymentController {
|
||||
private final RuntimeOrchestrator orchestrator;
|
||||
private final AppService appService;
|
||||
private final EnvironmentService environmentService;
|
||||
private final AuditService auditService;
|
||||
private final AppVersionRepository appVersionRepository;
|
||||
|
||||
public DeploymentController(DeploymentService deploymentService,
|
||||
DeploymentExecutor deploymentExecutor,
|
||||
RuntimeOrchestrator orchestrator,
|
||||
AppService appService,
|
||||
EnvironmentService environmentService) {
|
||||
EnvironmentService environmentService,
|
||||
AuditService auditService,
|
||||
AppVersionRepository appVersionRepository) {
|
||||
this.deploymentService = deploymentService;
|
||||
this.deploymentExecutor = deploymentExecutor;
|
||||
this.orchestrator = orchestrator;
|
||||
this.appService = appService;
|
||||
this.environmentService = environmentService;
|
||||
this.auditService = auditService;
|
||||
this.appVersionRepository = appVersionRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -86,13 +101,25 @@ public class DeploymentController {
|
||||
@ApiResponse(responseCode = "202", description = "Deployment accepted and starting")
|
||||
public ResponseEntity<Deployment> deploy(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@RequestBody DeployRequest request) {
|
||||
@RequestBody DeployRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), null);
|
||||
AppVersion appVersion = appVersionRepository.findById(request.appVersionId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("AppVersion not found: " + request.appVersionId()));
|
||||
Deployment deployment = deploymentService.createDeployment(app.id(), request.appVersionId(), env.id(), currentUserId());
|
||||
deploymentExecutor.executeAsync(deployment);
|
||||
auditService.log("deploy_app", AuditCategory.DEPLOYMENT, deployment.id().toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(),
|
||||
"appVersionId", request.appVersionId().toString(),
|
||||
"jarFilename", appVersion.jarFilename() != null ? appVersion.jarFilename() : "",
|
||||
"version", appVersion.version()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.accepted().body(deployment);
|
||||
} catch (IllegalArgumentException e) {
|
||||
auditService.log("deploy_app", AuditCategory.DEPLOYMENT, null,
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(), "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -103,12 +130,19 @@ public class DeploymentController {
|
||||
@ApiResponse(responseCode = "404", description = "Deployment not found")
|
||||
public ResponseEntity<Deployment> stop(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@PathVariable UUID deploymentId) {
|
||||
@PathVariable UUID deploymentId,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
Deployment deployment = deploymentService.getById(deploymentId);
|
||||
deploymentExecutor.stopDeployment(deployment);
|
||||
auditService.log("stop_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(deploymentService.getById(deploymentId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
auditService.log("stop_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "envSlug", env.slug(), "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -122,18 +156,25 @@ public class DeploymentController {
|
||||
public ResponseEntity<?> promote(@EnvPath Environment env,
|
||||
@PathVariable String appSlug,
|
||||
@PathVariable UUID deploymentId,
|
||||
@RequestBody PromoteRequest request) {
|
||||
@RequestBody PromoteRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
App sourceApp = appService.getByEnvironmentAndSlug(env.id(), appSlug);
|
||||
Deployment source = deploymentService.getById(deploymentId);
|
||||
Environment targetEnv = environmentService.getBySlug(request.targetEnvironment());
|
||||
// Target must also have the app with the same slug
|
||||
App targetApp = appService.getByEnvironmentAndSlug(targetEnv.id(), appSlug);
|
||||
Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id(), null);
|
||||
Deployment promoted = deploymentService.promote(targetApp.id(), source.appVersionId(), targetEnv.id(), currentUserId());
|
||||
deploymentExecutor.executeAsync(promoted);
|
||||
auditService.log("promote_deployment", AuditCategory.DEPLOYMENT, promoted.id().toString(),
|
||||
Map.of("sourceEnv", env.slug(), "targetEnv", request.targetEnvironment(),
|
||||
"appSlug", appSlug, "appVersionId", source.appVersionId().toString()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.accepted().body(promoted);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.status(org.springframework.http.HttpStatus.NOT_FOUND)
|
||||
auditService.log("promote_deployment", AuditCategory.DEPLOYMENT, deploymentId.toString(),
|
||||
Map.of("appSlug", appSlug, "error", e.getMessage()),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -157,6 +198,15 @@ public class DeploymentController {
|
||||
}
|
||||
}
|
||||
|
||||
private String currentUserId() {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getName() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No authentication");
|
||||
}
|
||||
String name = auth.getName();
|
||||
return name.startsWith("user:") ? name.substring(5) : name;
|
||||
}
|
||||
|
||||
public record DeployRequest(UUID appVersionId) {}
|
||||
public record PromoteRequest(String targetEnvironment) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user