feat: rewrite LicenseService to mint Ed25519-signed tokens
Replaces UUID token generation with LicenseMinter.mint() from cameleer-license-minter. Adds full-control generateLicense() overload accepting custom limits, expiry, grace period, and label. Adds verifyToken() and verifyTokenSignature() using LicenseValidator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
package net.siegeln.cameleer.saas.license;
|
package net.siegeln.cameleer.saas.license;
|
||||||
|
|
||||||
|
import com.cameleer.license.minter.LicenseMinter;
|
||||||
|
import com.cameleer.server.core.license.LicenseInfo;
|
||||||
|
import com.cameleer.server.core.license.LicenseValidator;
|
||||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||||
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -14,28 +20,52 @@ import java.util.UUID;
|
|||||||
@Service
|
@Service
|
||||||
public class LicenseService {
|
public class LicenseService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LicenseService.class);
|
||||||
|
|
||||||
private final LicenseRepository licenseRepository;
|
private final LicenseRepository licenseRepository;
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
|
private final SigningKeyService signingKeyService;
|
||||||
|
|
||||||
public LicenseService(LicenseRepository licenseRepository, AuditService auditService) {
|
public LicenseService(LicenseRepository licenseRepository,
|
||||||
|
AuditService auditService,
|
||||||
|
SigningKeyService signingKeyService) {
|
||||||
this.licenseRepository = licenseRepository;
|
this.licenseRepository = licenseRepository;
|
||||||
this.auditService = auditService;
|
this.auditService = auditService;
|
||||||
|
this.signingKeyService = signingKeyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
|
/**
|
||||||
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
|
* Mint an Ed25519-signed license with full control over limits.
|
||||||
|
*/
|
||||||
|
public LicenseEntity generateLicense(TenantEntity tenant,
|
||||||
|
Map<String, Integer> limits,
|
||||||
|
Instant expiresAt,
|
||||||
|
int gracePeriodDays,
|
||||||
|
String label,
|
||||||
|
UUID actorId) {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
Instant expiresAt = now.plus(validity);
|
UUID licenseId = UUID.randomUUID();
|
||||||
|
|
||||||
String token = UUID.randomUUID().toString();
|
LicenseInfo info = new LicenseInfo(
|
||||||
|
licenseId,
|
||||||
|
tenant.getSlug(),
|
||||||
|
label,
|
||||||
|
limits,
|
||||||
|
now,
|
||||||
|
expiresAt,
|
||||||
|
gracePeriodDays
|
||||||
|
);
|
||||||
|
|
||||||
|
String token = LicenseMinter.mint(info, signingKeyService.getPrivateKey());
|
||||||
|
|
||||||
var entity = new LicenseEntity();
|
var entity = new LicenseEntity();
|
||||||
entity.setTenantId(tenant.getId());
|
entity.setTenantId(tenant.getId());
|
||||||
entity.setTier(tenant.getTier().name());
|
entity.setTier(tenant.getTier().name());
|
||||||
entity.setLimits(new java.util.HashMap<>(limits));
|
entity.setLabel(label);
|
||||||
|
entity.setLimits(new HashMap<>(limits));
|
||||||
|
entity.setGracePeriodDays(gracePeriodDays);
|
||||||
entity.setIssuedAt(now);
|
entity.setIssuedAt(now);
|
||||||
entity.setExpiresAt(expiresAt);
|
entity.setExpiresAt(expiresAt);
|
||||||
entity.setGracePeriodDays(LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS);
|
|
||||||
entity.setToken(token);
|
entity.setToken(token);
|
||||||
|
|
||||||
var saved = licenseRepository.save(entity);
|
var saved = licenseRepository.save(entity);
|
||||||
@@ -47,6 +77,17 @@ public class LicenseService {
|
|||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload using tier presets and default validity.
|
||||||
|
*/
|
||||||
|
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
|
||||||
|
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
|
||||||
|
Instant expiresAt = Instant.now().plus(validity);
|
||||||
|
String label = tenant.getName() + " (" + tenant.getTier().name() + ")";
|
||||||
|
return generateLicense(tenant, limits, expiresAt,
|
||||||
|
LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS, label, actorId);
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<LicenseEntity> getActiveLicense(UUID tenantId) {
|
public Optional<LicenseEntity> getActiveLicense(UUID tenantId) {
|
||||||
return licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId);
|
return licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId);
|
||||||
}
|
}
|
||||||
@@ -63,18 +104,41 @@ public class LicenseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies a license token by checking its existence and validity in the database.
|
* Verify a signed license token using the stored public key.
|
||||||
* Returns the license entity's metadata as a map if found and not expired/revoked,
|
* Returns the parsed LicenseInfo if valid, or empty if invalid.
|
||||||
* or empty if the token is unknown or invalid.
|
|
||||||
*/
|
*/
|
||||||
public Optional<Map<String, Object>> verifyLicenseToken(String token) {
|
public Optional<LicenseInfo> verifyToken(String token, String expectedTenantId) {
|
||||||
return licenseRepository.findByToken(token)
|
try {
|
||||||
.filter(e -> e.getRevokedAt() == null)
|
String publicKeyB64 = signingKeyService.getPublicKeyBase64();
|
||||||
.filter(e -> e.getExpiresAt() == null || Instant.now().isBefore(e.getExpiresAt()))
|
LicenseValidator validator = new LicenseValidator(publicKeyB64, expectedTenantId);
|
||||||
.map(e -> Map.<String, Object>of(
|
LicenseInfo info = validator.validate(token);
|
||||||
"tenant_id", e.getTenantId().toString(),
|
return Optional.of(info);
|
||||||
"tier", e.getTier(),
|
} catch (Exception e) {
|
||||||
"limits", e.getLimits()
|
log.debug("License token verification failed: {}", e.getMessage());
|
||||||
));
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a signed license token without tenant ID check (for vendor verify tool).
|
||||||
|
* Decodes the payload and validates the signature only.
|
||||||
|
*/
|
||||||
|
public Optional<LicenseInfo> verifyTokenSignature(String token) {
|
||||||
|
try {
|
||||||
|
// Decode the payload portion to extract tenantId, then validate
|
||||||
|
String payloadB64 = token.split("\\.", 2)[0];
|
||||||
|
String payloadJson = new String(java.util.Base64.getDecoder().decode(payloadB64));
|
||||||
|
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
var tree = mapper.readTree(payloadJson);
|
||||||
|
String tenantId = tree.has("tid") ? tree.get("tid").asText() : tree.get("tenantId").asText();
|
||||||
|
|
||||||
|
String publicKeyB64 = signingKeyService.getPublicKeyBase64();
|
||||||
|
LicenseValidator validator = new LicenseValidator(publicKeyB64, tenantId);
|
||||||
|
LicenseInfo info = validator.validate(token);
|
||||||
|
return Optional.of(info);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("License token signature verification failed: {}", e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user