feat: add environment service with tier enforcement and audit logging
Implements EnvironmentService with full CRUD, duplicate slug rejection, tier-based environment count limits, and audit logging for create/update/delete. Adds ENVIRONMENT_CREATE, ENVIRONMENT_UPDATE, ENVIRONMENT_DELETE to AuditAction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package net.siegeln.cameleer.saas.audit;
|
||||
public enum AuditAction {
|
||||
AUTH_REGISTER, AUTH_LOGIN, AUTH_LOGIN_FAILED, AUTH_LOGOUT,
|
||||
TENANT_CREATE, TENANT_UPDATE, TENANT_SUSPEND, TENANT_REACTIVATE, TENANT_DELETE,
|
||||
ENVIRONMENT_CREATE, ENVIRONMENT_UPDATE, ENVIRONMENT_DELETE,
|
||||
APP_CREATE, APP_DEPLOY, APP_PROMOTE, APP_ROLLBACK, APP_SCALE, APP_STOP, APP_DELETE,
|
||||
SECRET_CREATE, SECRET_READ, SECRET_UPDATE, SECRET_DELETE, SECRET_ROTATE,
|
||||
CONFIG_UPDATE,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.siegeln.cameleer.saas.environment;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.license.LicenseDefaults;
|
||||
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
||||
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class EnvironmentService {
|
||||
|
||||
private final EnvironmentRepository environmentRepository;
|
||||
private final LicenseRepository licenseRepository;
|
||||
private final AuditService auditService;
|
||||
private final RuntimeConfig runtimeConfig;
|
||||
|
||||
public EnvironmentService(EnvironmentRepository environmentRepository,
|
||||
LicenseRepository licenseRepository,
|
||||
AuditService auditService,
|
||||
RuntimeConfig runtimeConfig) {
|
||||
this.environmentRepository = environmentRepository;
|
||||
this.licenseRepository = licenseRepository;
|
||||
this.auditService = auditService;
|
||||
this.runtimeConfig = runtimeConfig;
|
||||
}
|
||||
|
||||
public EnvironmentEntity create(UUID tenantId, String slug, String displayName, UUID actorId) {
|
||||
if (environmentRepository.existsByTenantIdAndSlug(tenantId, slug)) {
|
||||
throw new IllegalArgumentException("Slug already exists for this tenant: " + slug);
|
||||
}
|
||||
|
||||
enforceTierLimit(tenantId);
|
||||
|
||||
var entity = new EnvironmentEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setSlug(slug);
|
||||
entity.setDisplayName(displayName);
|
||||
entity.setBootstrapToken(runtimeConfig.getBootstrapToken());
|
||||
|
||||
var saved = environmentRepository.save(entity);
|
||||
|
||||
auditService.log(actorId, null, tenantId,
|
||||
AuditAction.ENVIRONMENT_CREATE, slug,
|
||||
null, null, "SUCCESS", null);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public EnvironmentEntity createDefaultForTenant(UUID tenantId) {
|
||||
return environmentRepository.findByTenantIdAndSlug(tenantId, "default")
|
||||
.orElseGet(() -> create(tenantId, "default", "Default", null));
|
||||
}
|
||||
|
||||
public List<EnvironmentEntity> listByTenantId(UUID tenantId) {
|
||||
return environmentRepository.findByTenantId(tenantId);
|
||||
}
|
||||
|
||||
public Optional<EnvironmentEntity> getById(UUID id) {
|
||||
return environmentRepository.findById(id);
|
||||
}
|
||||
|
||||
public EnvironmentEntity updateDisplayName(UUID environmentId, String displayName, UUID actorId) {
|
||||
var entity = environmentRepository.findById(environmentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Environment not found: " + environmentId));
|
||||
|
||||
entity.setDisplayName(displayName);
|
||||
var saved = environmentRepository.save(entity);
|
||||
|
||||
auditService.log(actorId, null, entity.getTenantId(),
|
||||
AuditAction.ENVIRONMENT_UPDATE, entity.getSlug(),
|
||||
null, null, "SUCCESS", null);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public void delete(UUID environmentId, UUID actorId) {
|
||||
var entity = environmentRepository.findById(environmentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Environment not found: " + environmentId));
|
||||
|
||||
if ("default".equals(entity.getSlug())) {
|
||||
throw new IllegalStateException("Cannot delete the default environment");
|
||||
}
|
||||
|
||||
environmentRepository.delete(entity);
|
||||
|
||||
auditService.log(actorId, null, entity.getTenantId(),
|
||||
AuditAction.ENVIRONMENT_DELETE, entity.getSlug(),
|
||||
null, null, "SUCCESS", null);
|
||||
}
|
||||
|
||||
private void enforceTierLimit(UUID tenantId) {
|
||||
var license = licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId);
|
||||
if (license.isEmpty()) {
|
||||
throw new IllegalStateException("No active license");
|
||||
}
|
||||
var limits = LicenseDefaults.limitsForTier(Tier.valueOf(license.get().getTier()));
|
||||
var maxEnvs = (int) limits.getOrDefault("max_environments", 1);
|
||||
var currentCount = environmentRepository.countByTenantId(tenantId);
|
||||
if (maxEnvs != -1 && currentCount >= maxEnvs) {
|
||||
throw new IllegalStateException("Environment limit reached for current tier");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user