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:
@@ -5,37 +5,28 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class LicenseGateTest {
|
class LicenseGateTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void noLicense_allFeaturesEnabled() {
|
void noLicense_returnsOpenTier() {
|
||||||
LicenseGate gate = new LicenseGate();
|
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.getTier()).isEqualTo("open");
|
||||||
|
assertThat(gate.getLimit("max_apps", 99)).isEqualTo(99);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withLicense_onlyLicensedFeaturesEnabled() {
|
void loaded_exposesLimits() {
|
||||||
LicenseGate gate = new LicenseGate();
|
LicenseGate gate = new LicenseGate();
|
||||||
LicenseInfo license = new LicenseInfo("MID",
|
LicenseInfo info = new LicenseInfo("MID",
|
||||||
Set.of(Feature.topology, Feature.lineage, Feature.correlation),
|
|
||||||
Map.of("max_agents", 10, "retention_days", 30),
|
Map.of("max_agents", 10, "retention_days", 30),
|
||||||
Instant.now(), Instant.now().plus(365, ChronoUnit.DAYS));
|
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.getTier()).isEqualTo("MID");
|
||||||
assertThat(gate.getLimit("max_agents", 0)).isEqualTo(10);
|
assertThat(gate.getLimit("max_agents", 0)).isEqualTo(10);
|
||||||
|
assertThat(gate.getLimit("missing", 7)).isEqualTo(7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class LicenseValidatorTest {
|
|||||||
|
|
||||||
Instant expires = Instant.now().plus(365, ChronoUnit.DAYS);
|
Instant expires = Instant.now().plus(365, ChronoUnit.DAYS);
|
||||||
String payload = """
|
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();
|
""".formatted(Instant.now().getEpochSecond(), expires.getEpochSecond()).trim();
|
||||||
String signature = sign(kp.getPrivate(), payload);
|
String signature = sign(kp.getPrivate(), payload);
|
||||||
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
||||||
@@ -41,8 +41,6 @@ class LicenseValidatorTest {
|
|||||||
LicenseInfo info = validator.validate(token);
|
LicenseInfo info = validator.validate(token);
|
||||||
|
|
||||||
assertThat(info.tier()).isEqualTo("HIGH");
|
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.getLimit("max_agents", 0)).isEqualTo(50);
|
||||||
assertThat(info.isExpired()).isFalse();
|
assertThat(info.isExpired()).isFalse();
|
||||||
}
|
}
|
||||||
@@ -55,7 +53,7 @@ class LicenseValidatorTest {
|
|||||||
|
|
||||||
Instant past = Instant.now().minus(1, ChronoUnit.DAYS);
|
Instant past = Instant.now().minus(1, ChronoUnit.DAYS);
|
||||||
String payload = """
|
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();
|
""".formatted(past.minus(30, ChronoUnit.DAYS).getEpochSecond(), past.getEpochSecond()).trim();
|
||||||
String signature = sign(kp.getPrivate(), payload);
|
String signature = sign(kp.getPrivate(), payload);
|
||||||
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
String token = Base64.getEncoder().encodeToString(payload.getBytes()) + "." + signature;
|
||||||
@@ -72,7 +70,7 @@ class LicenseValidatorTest {
|
|||||||
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
|
LicenseValidator validator = new LicenseValidator(publicKeyBase64);
|
||||||
|
|
||||||
String payload = """
|
String payload = """
|
||||||
{"tier":"LOW","features":["topology"],"limits":{},"iat":0,"exp":9999999999}
|
{"tier":"LOW","limits":{},"iat":0,"exp":9999999999}
|
||||||
""".trim();
|
""".trim();
|
||||||
String signature = sign(kp.getPrivate(), payload);
|
String signature = sign(kp.getPrivate(), payload);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.cameleer.server.core.license;
|
|
||||||
|
|
||||||
public enum Feature {
|
|
||||||
topology,
|
|
||||||
lineage,
|
|
||||||
correlation,
|
|
||||||
debugger,
|
|
||||||
replay
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,8 @@ public class LicenseGate {
|
|||||||
|
|
||||||
public void load(LicenseInfo license) {
|
public void load(LicenseInfo license) {
|
||||||
current.set(license);
|
current.set(license);
|
||||||
log.info("License loaded: tier={}, features={}, expires={}",
|
log.info("License loaded: tier={}, limits={}, expires={}",
|
||||||
license.tier(), license.features(), license.expiresAt());
|
license.tier(), license.limits(), license.expiresAt());
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled(Feature feature) {
|
|
||||||
return current.get().hasFeature(feature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTier() {
|
public String getTier() {
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package com.cameleer.server.core.license;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public record LicenseInfo(
|
public record LicenseInfo(
|
||||||
String tier,
|
String tier,
|
||||||
Set<Feature> features,
|
|
||||||
Map<String, Integer> limits,
|
Map<String, Integer> limits,
|
||||||
Instant issuedAt,
|
Instant issuedAt,
|
||||||
Instant expiresAt
|
Instant expiresAt
|
||||||
@@ -15,16 +13,11 @@ public record LicenseInfo(
|
|||||||
return expiresAt != null && Instant.now().isAfter(expiresAt);
|
return expiresAt != null && Instant.now().isAfter(expiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasFeature(Feature feature) {
|
|
||||||
return features.contains(feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLimit(String key, int defaultValue) {
|
public int getLimit(String key, int defaultValue) {
|
||||||
return limits.getOrDefault(key, defaultValue);
|
return limits.getOrDefault(key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open license — all features enabled, no limits. Used when no license is configured. */
|
|
||||||
public static LicenseInfo open() {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,17 +56,6 @@ public class LicenseValidator {
|
|||||||
|
|
||||||
String tier = root.get("tier").asText();
|
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<>();
|
Map<String, Integer> limits = new HashMap<>();
|
||||||
if (root.has("limits")) {
|
if (root.has("limits")) {
|
||||||
root.get("limits").fields().forEachRemaining(entry ->
|
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 issuedAt = root.has("iat") ? Instant.ofEpochSecond(root.get("iat").asLong()) : Instant.now();
|
||||||
Instant expiresAt = root.has("exp") ? Instant.ofEpochSecond(root.get("exp").asLong()) : null;
|
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()) {
|
if (info.isExpired()) {
|
||||||
throw new IllegalArgumentException("License expired at " + expiresAt);
|
throw new IllegalArgumentException("License expired at " + expiresAt);
|
||||||
|
|||||||
Reference in New Issue
Block a user