feat(audit): add audit logging to vendor server ops and audit_log immutability migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-29 11:31:46 +02:00
parent da52707aec
commit cb411ff337
3 changed files with 36 additions and 6 deletions

View File

@@ -152,9 +152,10 @@ public class VendorTenantController {
}
@PostMapping("/{id}/restart")
public ResponseEntity<Void> restart(@PathVariable UUID id) {
public ResponseEntity<Void> restart(@PathVariable UUID id,
@AuthenticationPrincipal Jwt jwt) {
try {
vendorTenantService.restartServer(id);
vendorTenantService.restartServer(id, resolveActorId(jwt));
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
@@ -162,9 +163,10 @@ public class VendorTenantController {
}
@PostMapping("/{id}/upgrade")
public ResponseEntity<Void> upgrade(@PathVariable UUID id) {
public ResponseEntity<Void> upgrade(@PathVariable UUID id,
@AuthenticationPrincipal Jwt jwt) {
try {
vendorTenantService.upgradeServer(id);
vendorTenantService.upgradeServer(id, resolveActorId(jwt));
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();

View File

@@ -304,7 +304,7 @@ public class VendorTenantService {
return serverApiClient.getHealth(endpoint);
}
public void restartServer(UUID tenantId) {
public void restartServer(UUID tenantId, UUID actorId) {
TenantEntity tenant = tenantService.getById(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
if (!tenantProvisioner.isAvailable()) return;
@@ -319,13 +319,19 @@ public class VendorTenantService {
var license = licenseService.getActiveLicense(tenantId).orElse(null);
String token = license != null ? license.getToken() : "";
self.provisionAsync(tenantId, tenant.getSlug(), tenant.getTier().name(), token, null);
auditService.log(actorId, null, tenantId,
AuditAction.SERVER_RESTARTED, tenant.getSlug(),
null, null, "SUCCESS", null);
return;
}
throw e;
}
auditService.log(actorId, null, tenantId,
AuditAction.SERVER_RESTARTED, tenant.getSlug(),
null, null, "SUCCESS", null);
}
public void upgradeServer(UUID tenantId) {
public void upgradeServer(UUID tenantId, UUID actorId) {
TenantEntity tenant = tenantService.getById(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
if (!tenantProvisioner.isAvailable()) return;
@@ -336,6 +342,9 @@ public class VendorTenantService {
var license = licenseService.getActiveLicense(tenantId).orElse(null);
String token = license != null ? license.getToken() : "";
self.provisionAsync(tenantId, tenant.getSlug(), tenant.getTier().name(), token, null);
auditService.log(actorId, null, tenantId,
AuditAction.SERVER_UPGRADED, tenant.getSlug(),
null, null, "SUCCESS", null);
}
@Transactional

View File

@@ -0,0 +1,19 @@
-- V006: Protect audit_log from tampering (SOC 2 CC7.2/CC7.3)
-- Prevents UPDATE and DELETE on audit_log rows via database triggers.
CREATE OR REPLACE FUNCTION audit_log_prevent_modify()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'audit_log is immutable: % operations are not allowed', TG_OP;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_log_no_update
BEFORE UPDATE ON audit_log
FOR EACH ROW
EXECUTE FUNCTION audit_log_prevent_modify();
CREATE TRIGGER audit_log_no_delete
BEFORE DELETE ON audit_log
FOR EACH ROW
EXECUTE FUNCTION audit_log_prevent_modify();