fix(license): explicit @Autowired ctor + tolerate audit failures

Two follow-ups to LicenseEnforcer review:
- Add @Autowired to the 3-arg ctor so Spring picks it unambiguously
  (the 2-arg test ctor is otherwise an equally-greedy candidate).
- Wrap audit.log() in try/catch + log.warn so a degraded audit DB
  cannot mask a cap rejection: callers still see HTTP 403 even when
  audit storage is unhealthy.
- Extract counter name to private static final.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 12:43:27 +02:00
parent 4985348827
commit 9b9b56043c

View File

@@ -8,6 +8,9 @@ import com.cameleer.server.core.license.LicenseLimits;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -30,11 +33,15 @@ import java.util.concurrent.ConcurrentMap;
@Component @Component
public class LicenseEnforcer { public class LicenseEnforcer {
private static final Logger log = LoggerFactory.getLogger(LicenseEnforcer.class);
private static final String COUNTER_NAME = "cameleer_license_cap_rejections_total";
private final LicenseGate gate; private final LicenseGate gate;
private final MeterRegistry meters; private final MeterRegistry meters;
private final AuditService audit; private final AuditService audit;
private final ConcurrentMap<String, Counter> rejectionCounters = new ConcurrentHashMap<>(); private final ConcurrentMap<String, Counter> rejectionCounters = new ConcurrentHashMap<>();
@Autowired
public LicenseEnforcer(LicenseGate gate, MeterRegistry meters, AuditService audit) { public LicenseEnforcer(LicenseGate gate, MeterRegistry meters, AuditService audit) {
this.gate = gate; this.gate = gate;
this.meters = meters; this.meters = meters;
@@ -51,9 +58,10 @@ public class LicenseEnforcer {
int cap = effective.get(limitKey); // throws IllegalArgumentException if unknown key int cap = effective.get(limitKey); // throws IllegalArgumentException if unknown key
long projected = currentUsage + requestedDelta; long projected = currentUsage + requestedDelta;
if (projected > cap) { if (projected > cap) {
rejectionCounters.computeIfAbsent(limitKey, k -> Counter.builder("cameleer_license_cap_rejections_total") rejectionCounters.computeIfAbsent(limitKey, k -> Counter.builder(COUNTER_NAME)
.tag("limit", k).register(meters)).increment(); .tag("limit", k).register(meters)).increment();
if (audit != null) { if (audit != null) {
try {
Map<String, Object> detail = new LinkedHashMap<>(); Map<String, Object> detail = new LinkedHashMap<>();
detail.put("limit", limitKey); detail.put("limit", limitKey);
detail.put("current", currentUsage); detail.put("current", currentUsage);
@@ -61,6 +69,10 @@ public class LicenseEnforcer {
detail.put("cap", cap); detail.put("cap", cap);
detail.put("state", gate.getState().name()); detail.put("state", gate.getState().name());
audit.log("system", "cap_exceeded", AuditCategory.LICENSE, limitKey, detail, AuditResult.FAILURE, null); audit.log("system", "cap_exceeded", AuditCategory.LICENSE, limitKey, detail, AuditResult.FAILURE, null);
} catch (RuntimeException e) {
// Audit storage degraded; log and continue so the cap rejection still surfaces as 403.
log.warn("Failed to write cap_exceeded audit row for limit={}: {}", limitKey, e.toString());
}
} }
throw new LicenseCapExceededException(limitKey, projected, cap); throw new LicenseCapExceededException(limitKey, projected, cap);
} }