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 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-15 00:16:06 +02:00
parent b79a7fe405
commit c1458e4995

View File

@@ -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