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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user