From cb411ff3377c72e5baa2a7865f3cb8a0cc12f6e1 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:31:46 +0200 Subject: [PATCH] feat(audit): add audit logging to vendor server ops and audit_log immutability migration Co-Authored-By: Claude Opus 4.6 (1M context) --- .../saas/vendor/VendorTenantController.java | 10 ++++++---- .../saas/vendor/VendorTenantService.java | 13 +++++++++++-- .../V006__audit_log_immutability.sql | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/db/migration/V006__audit_log_immutability.sql diff --git a/src/main/java/io/cameleer/saas/vendor/VendorTenantController.java b/src/main/java/io/cameleer/saas/vendor/VendorTenantController.java index e0e7e91..ebea994 100644 --- a/src/main/java/io/cameleer/saas/vendor/VendorTenantController.java +++ b/src/main/java/io/cameleer/saas/vendor/VendorTenantController.java @@ -152,9 +152,10 @@ public class VendorTenantController { } @PostMapping("/{id}/restart") - public ResponseEntity restart(@PathVariable UUID id) { + public ResponseEntity 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 upgrade(@PathVariable UUID id) { + public ResponseEntity 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(); diff --git a/src/main/java/io/cameleer/saas/vendor/VendorTenantService.java b/src/main/java/io/cameleer/saas/vendor/VendorTenantService.java index a0dc9c4..256d39d 100644 --- a/src/main/java/io/cameleer/saas/vendor/VendorTenantService.java +++ b/src/main/java/io/cameleer/saas/vendor/VendorTenantService.java @@ -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 diff --git a/src/main/resources/db/migration/V006__audit_log_immutability.sql b/src/main/resources/db/migration/V006__audit_log_immutability.sql new file mode 100644 index 0000000..71ffff8 --- /dev/null +++ b/src/main/resources/db/migration/V006__audit_log_immutability.sql @@ -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();