From c1458e499580d4b0322bc4b4f4dcdaba3bfd3792 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:16:06 +0200 Subject: [PATCH] feat: create per-tenant PG database during provisioning, drop on delete Inject TenantDatabaseService; call createTenantDatabase() at the start of provisionAsync() (stores generated password on TenantEntity), and dropTenantDatabase() in delete() before GDPR data erasure. Co-Authored-By: Claude Sonnet 4.6 --- .../saas/vendor/VendorTenantService.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java index 0fe19d3..adb4c6c 100644 --- a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java +++ b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java @@ -6,6 +6,7 @@ import net.siegeln.cameleer.saas.identity.LogtoConfig; import net.siegeln.cameleer.saas.identity.LogtoManagementClient; import net.siegeln.cameleer.saas.identity.ServerApiClient; import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties; +import net.siegeln.cameleer.saas.provisioning.TenantDatabaseService; import net.siegeln.cameleer.saas.provisioning.TenantDataCleanupService; import net.siegeln.cameleer.saas.identity.ServerApiClient.ServerHealthResponse; import net.siegeln.cameleer.saas.license.LicenseEntity; @@ -47,6 +48,7 @@ public class VendorTenantService { private final AuditService auditService; private final ProvisioningProperties provisioningProps; private final TenantDataCleanupService dataCleanupService; + private final TenantDatabaseService tenantDatabaseService; public VendorTenantService(TenantService tenantService, TenantRepository tenantRepository, @@ -57,7 +59,8 @@ public class VendorTenantService { LogtoConfig logtoConfig, AuditService auditService, ProvisioningProperties provisioningProps, - TenantDataCleanupService dataCleanupService) { + TenantDataCleanupService dataCleanupService, + TenantDatabaseService tenantDatabaseService) { this.tenantService = tenantService; this.tenantRepository = tenantRepository; this.licenseService = licenseService; @@ -68,6 +71,7 @@ public class VendorTenantService { this.auditService = auditService; this.provisioningProps = provisioningProps; this.dataCleanupService = dataCleanupService; + this.tenantDatabaseService = tenantDatabaseService; } @Transactional @@ -119,7 +123,30 @@ public class VendorTenantService { @Async public void provisionAsync(UUID tenantId, String slug, String tier, String licenseToken, UUID actorId) { try { - var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken); + // Create per-tenant PG user + schema + String dbPassword = java.util.UUID.randomUUID().toString().replace("-", "") + + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8); + try { + tenantDatabaseService.createTenantDatabase(slug, dbPassword); + } catch (Exception e) { + log.error("Failed to create tenant database for {}: {}", slug, e.getMessage(), e); + tenantRepository.findById(tenantId).ifPresent(t -> { + t.setProvisionError("Database setup failed: " + e.getMessage()); + tenantRepository.save(t); + }); + return; + } + + // Store DB password on entity + TenantEntity tenantForDb = tenantRepository.findById(tenantId).orElse(null); + if (tenantForDb == null) { + log.error("Tenant {} disappeared during provisioning", slug); + return; + } + tenantForDb.setDbPassword(dbPassword); + tenantRepository.save(tenantForDb); + + var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword); ProvisionResult result = tenantProvisioner.provision(provisionRequest); TenantEntity tenant = tenantRepository.findById(tenantId).orElse(null); @@ -302,7 +329,15 @@ public class VendorTenantService { } } + // Drop per-tenant PG user + database + try { + tenantDatabaseService.dropTenantDatabase(tenant.getSlug()); + } catch (Exception e) { + log.warn("Failed to drop tenant database for {}: {}", tenant.getSlug(), e.getMessage()); + } + // Erase tenant data from server databases (GDPR) + // TODO: split into cleanupClickHouse() in next task dataCleanupService.cleanup(tenant.getSlug()); // Soft-delete