refactor(license): remove dead Feature enum and isEnabled scaffolding

Spec §9 — feature flags are out of scope for license enforcement.
Drops Feature.java, LicenseGate.isEnabled, LicenseInfo.hasFeature,
and the corresponding test cases. LicenseValidator now silently
ignores any features array on the wire (no error).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 10:21:51 +02:00
parent ec51aef802
commit 551a7f12b5
6 changed files with 13 additions and 55 deletions

View File

@@ -5,37 +5,28 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class LicenseGateTest {
@Test
void noLicense_allFeaturesEnabled() {
void noLicense_returnsOpenTier() {
LicenseGate gate = new LicenseGate();
// No license loaded -> open mode
assertThat(gate.isEnabled(Feature.debugger)).isTrue();
assertThat(gate.isEnabled(Feature.replay)).isTrue();
assertThat(gate.isEnabled(Feature.lineage)).isTrue();
assertThat(gate.getTier()).isEqualTo("open");
assertThat(gate.getLimit("max_apps", 99)).isEqualTo(99);
}
@Test
void withLicense_onlyLicensedFeaturesEnabled() {
void loaded_exposesLimits() {
LicenseGate gate = new LicenseGate();
LicenseInfo license = new LicenseInfo("MID",
Set.of(Feature.topology, Feature.lineage, Feature.correlation),
LicenseInfo info = new LicenseInfo("MID",
Map.of("max_agents", 10, "retention_days", 30),
Instant.now(), Instant.now().plus(365, ChronoUnit.DAYS));
gate.load(license);
gate.load(info);
assertThat(gate.isEnabled(Feature.topology)).isTrue();
assertThat(gate.isEnabled(Feature.lineage)).isTrue();
assertThat(gate.isEnabled(Feature.debugger)).isFalse();
assertThat(gate.isEnabled(Feature.replay)).isFalse();
assertThat(gate.getTier()).isEqualTo("MID");
assertThat(gate.getLimit("max_agents", 0)).isEqualTo(10);
assertThat(gate.getLimit("missing", 7)).isEqualTo(7);
}
}

View File

@@ -33,7 +33,7 @@ class LicenseValidatorTest {
Instant expires = Instant.now().plus(365, ChronoUnit.DAYS);
String payload = """
{"tier":"HIGH","features":["topology","lineage","debugger"],"limits":{"max_agents":50,"retention_days":90},"iat":%d,"exp":%d}
{"tier":"HIGH","limits":{"max_agents":50,"retention_days":90},"iat":%d,"exp":%d}
""".formatted(Instant.now().getEpochSecond(), expires.getEpochSecond()).trim();
String signature = sign(kp.getPrivate(), payload);
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
@@ -41,8 +41,6 @@ class LicenseValidatorTest {
LicenseInfo info = validator.validate(token);
assertThat(info.tier()).isEqualTo("HIGH");
assertThat(info.hasFeature(Feature.debugger)).isTrue();
assertThat(info.hasFeature(Feature.replay)).isFalse();
assertThat(info.getLimit("max_agents", 0)).isEqualTo(50);
assertThat(info.isExpired()).isFalse();
}
@@ -55,7 +53,7 @@ class LicenseValidatorTest {
Instant past = Instant.now().minus(1, ChronoUnit.DAYS);
String payload = """
{"tier":"LOW","features":["topology"],"limits":{},"iat":%d,"exp":%d}
{"tier":"LOW","limits":{},"iat":%d,"exp":%d}
""".formatted(past.minus(30, ChronoUnit.DAYS).getEpochSecond(), past.getEpochSecond()).trim();
String signature = sign(kp.getPrivate(), payload);
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
@@ -72,7 +70,7 @@ class LicenseValidatorTest {
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
String payload = """
{"tier":"LOW","features":["topology"],"limits":{},"iat":0,"exp":9999999999}
{"tier":"LOW","limits":{},"iat":0,"exp":9999999999}
""".trim();
String signature = sign(kp.getPrivate(), payload);

View File

@@ -1,9 +0,0 @@
package com.cameleer.server.core.license;
public enum Feature {
topology,
lineage,
correlation,
debugger,
replay
}

View File

@@ -13,12 +13,8 @@ public class LicenseGate {
public void load(LicenseInfo license) {
current.set(license);
log.info("License loaded: tier={}, features={}, expires={}",
license.tier(), license.features(), license.expiresAt());
}
public boolean isEnabled(Feature feature) {
return current.get().hasFeature(feature);
log.info("License loaded: tier={}, limits={}, expires={}",
license.tier(), license.limits(), license.expiresAt());
}
public String getTier() {

View File

@@ -2,11 +2,9 @@ package com.cameleer.server.core.license;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
public record LicenseInfo(
String tier,
Set<Feature> features,
Map<String, Integer> limits,
Instant issuedAt,
Instant expiresAt
@@ -15,16 +13,11 @@ public record LicenseInfo(
return expiresAt != null && Instant.now().isAfter(expiresAt);
}
public boolean hasFeature(Feature feature) {
return features.contains(feature);
}
public int getLimit(String key, int defaultValue) {
return limits.getOrDefault(key, defaultValue);
}
/** Open license — all features enabled, no limits. Used when no license is configured. */
public static LicenseInfo open() {
return new LicenseInfo("open", Set.of(Feature.values()), Map.of(), Instant.now(), null);
return new LicenseInfo("open", Map.of(), Instant.now(), null);
}
}

View File

@@ -56,17 +56,6 @@ public class LicenseValidator {
String tier = root.get("tier").asText();
Set<Feature> features = new HashSet<>();
if (root.has("features")) {
for (JsonNode f : root.get("features")) {
try {
features.add(Feature.valueOf(f.asText()));
} catch (IllegalArgumentException e) {
log.warn("Unknown feature in license: {}", f.asText());
}
}
}
Map<String, Integer> limits = new HashMap<>();
if (root.has("limits")) {
root.get("limits").fields().forEachRemaining(entry ->
@@ -76,7 +65,7 @@ public class LicenseValidator {
Instant issuedAt = root.has("iat") ? Instant.ofEpochSecond(root.get("iat").asLong()) : Instant.now();
Instant expiresAt = root.has("exp") ? Instant.ofEpochSecond(root.get("exp").asLong()) : null;
LicenseInfo info = new LicenseInfo(tier, features, limits, issuedAt, expiresAt);
LicenseInfo info = new LicenseInfo(tier, limits, issuedAt, expiresAt);
if (info.isExpired()) {
throw new IllegalArgumentException("License expired at " + expiresAt);