From ce2aab3f2f32323a11dcc95b2a3e4741d1d38515 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 17 Dec 2019 14:58:58 +1100 Subject: [PATCH] Add setting to restrict license types (#50252) This adds a new "xpack.license.upload.types" setting that restricts which license types may be uploaded to a cluster. By default all types are allowed (excluding basic, which can only be generated and never uploaded). This setting does not restrict APIs that generate licenses such as the start trial API. This setting is not documented as it is intended to be set by orchestrators and not end users. Backport of: #49418 --- .../org/elasticsearch/license/License.java | 49 ++++-- .../elasticsearch/license/LicenseService.java | 49 +++++- .../license/OperationModeFileWatcher.java | 2 +- .../license/RemoteClusterLicenseChecker.java | 6 +- .../xpack/core/XPackClientPlugin.java | 1 + .../core/ml/inference/TrainedModelConfig.java | 4 +- .../license/LicenseFIPSTests.java | 10 ++ .../license/LicenseOperationModeTests.java | 6 +- .../LicenseOperationModeUpdateTests.java | 2 +- .../license/LicenseServiceTests.java | 164 ++++++++++++++++++ 10 files changed, 273 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java index 9351f36f913..b59ecf9bebd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java @@ -63,7 +63,23 @@ public class License implements ToXContentObject { /** * Backward compatible license type parsing for older license models */ - public static LicenseType resolve(String name) { + public static LicenseType resolve(License license) { + if (license.version == VERSION_START) { + // in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum + return resolve(license.subscriptionType); + } else { + // in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum + // in 5.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum + // in 6.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum + // in 7.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum | enterprise + return resolve(license.type); + } + } + + /** + * Backward compatible license type parsing for older license models + */ + static LicenseType resolve(String name) { switch (name.toLowerCase(Locale.ROOT)) { case "missing": return null; @@ -171,8 +187,12 @@ public class License implements ToXContentObject { return Integer.compare(opMode1.id, opMode2.id); } - public static OperationMode resolve(String typeName) { - LicenseType type = LicenseType.resolve(typeName); + /** + * Determine the operating mode for a license type + * @see LicenseType#resolve(License) + * @see #parse(String) + */ + public static OperationMode resolve(LicenseType type) { if (type == null) { return MISSING; } @@ -193,6 +213,21 @@ public class License implements ToXContentObject { } } + /** + * Parses an {@code OperatingMode} from a String. + * The string must name an operating mode, and not a licensing level (that is, it cannot parse old style license levels + * such as "dev" or "silver"). + * @see #description() + */ + public static OperationMode parse(String mode) { + try { + return OperationMode.valueOf(mode.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("unrecognised license operating mode [ " + mode + "], supported modes are [" + + Stream.of(values()).map(OperationMode::description).collect(Collectors.joining(",")) + "]"); + } + } + public String description() { return name().toLowerCase(Locale.ROOT); } @@ -218,13 +253,7 @@ public class License implements ToXContentObject { } this.maxNodes = maxNodes; this.startDate = startDate; - if (version == VERSION_START) { - // in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum - this.operationMode = OperationMode.resolve(subscriptionType); - } else { - // in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum - this.operationMode = OperationMode.resolve(type); - } + this.operationMode = OperationMode.resolve(LicenseType.resolve(this)); validate(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index f049d230378..aeb7fe5fcef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -48,6 +48,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Service responsible for managing {@link LicensesMetaData}. @@ -65,6 +66,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste return SelfGeneratedLicense.validateSelfGeneratedType(type); }, Setting.Property.NodeScope); + static final List ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes(); + + public static final Setting> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting("xpack.license.upload.types", + Collections.unmodifiableList(ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.toList())), + License.LicenseType::parse, LicenseService::validateUploadTypesSetting, Setting.Property.NodeScope); + // pkg private for tests static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); @@ -105,6 +112,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste */ private List expirationCallbacks = new ArrayList<>(); + /** + * Which license types are permitted to be uploaded to the cluster + * @see #ALLOWED_LICENSE_TYPES_SETTING + */ + private final List allowedLicenseTypes; + /** * Max number of nodes licensed by generated trial license */ @@ -124,6 +137,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste this.clock = clock; this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; + this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES_SETTING.get(settings); this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, () -> updateLicenseState(getLicensesMetaData())); @@ -197,8 +211,20 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste final long now = clock.millis(); if (!LicenseVerifier.verifyLicense(newLicense) || newLicense.issueDate() > now || newLicense.startDate() > now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID)); - } else if (newLicense.type().equals(License.LicenseType.BASIC.getTypeName())) { + return; + } + final License.LicenseType licenseType; + try { + licenseType = License.LicenseType.resolve(newLicense); + } catch (Exception e) { + listener.onFailure(e); + return; + } + if (licenseType == License.LicenseType.BASIC) { listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed.")); + } else if (isAllowedLicenseType(licenseType) == false) { + listener.onFailure(new IllegalArgumentException( + "Registering [" + licenseType.getTypeName() + "] licenses is not allowed on this cluster")); } else if (newLicense.expiryDate() < now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED)); } else { @@ -273,6 +299,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste } } + private boolean isAllowedLicenseType(License.LicenseType type) { + logger.debug("Checking license [{}] against allowed license types: {}", type, allowedLicenseTypes); + return allowedLicenseTypes.contains(type); + } + public static Map getAckMessages(License newLicense, License currentLicense) { Map acknowledgeMessages = new HashMap<>(); if (!License.isAutoGeneratedLicense(currentLicense.signature()) // current license is not auto-generated @@ -575,4 +606,20 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste private static boolean isBoundToLoopback(DiscoveryNode localNode) { return localNode.getAddress().address().getAddress().isLoopbackAddress(); } + + private static List getAllowableUploadTypes() { + return Collections.unmodifiableList(Stream.of(License.LicenseType.values()) + .filter(t -> t != License.LicenseType.BASIC) + .collect(Collectors.toList())); + } + + private static void validateUploadTypesSetting(List value) { + if (ALLOWABLE_UPLOAD_TYPES.containsAll(value) == false) { + throw new IllegalArgumentException("Invalid value [" + + value.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + + "] for " + ALLOWED_LICENSE_TYPES_SETTING.getKey() + ", allowed values are [" + + ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + + "]"); + } + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java index b8e6446b9f4..ee08b9f7330 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java @@ -106,7 +106,7 @@ public final class OperationModeFileWatcher implements FileChangesListener { // this UTF-8 conversion is much pickier than java String final String operationMode = new BytesRef(content).utf8ToString(); try { - newOperationMode = OperationMode.resolve(operationMode); + newOperationMode = OperationMode.parse(operationMode); } catch (IllegalArgumentException e) { logger.error( (Supplier) () -> new ParameterizedMessage( diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java index 7d5a3b5e9a5..5de1186767f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java @@ -138,7 +138,7 @@ public final class RemoteClusterLicenseChecker { } public static boolean isLicensePlatinumOrTrial(final XPackInfoResponse.LicenseInfo licenseInfo) { - final License.OperationMode mode = License.OperationMode.resolve(licenseInfo.getMode()); + final License.OperationMode mode = License.OperationMode.parse(licenseInfo.getMode()); return mode == License.OperationMode.PLATINUM || mode == License.OperationMode.TRIAL; } @@ -168,7 +168,7 @@ public final class RemoteClusterLicenseChecker { return; } if ((licenseInfo.getStatus() == LicenseStatus.ACTIVE) == false - || predicate.test(License.OperationMode.resolve(licenseInfo.getMode())) == false) { + || predicate.test(License.OperationMode.parse(licenseInfo.getMode())) == false) { listener.onResponse(LicenseCheck.failure(new RemoteClusterLicenseInfo(clusterAlias.get(), licenseInfo))); return; } @@ -282,7 +282,7 @@ public final class RemoteClusterLicenseChecker { final String message = String.format( Locale.ROOT, "the license mode [%s] on cluster [%s] does not enable [%s]", - License.OperationMode.resolve(remoteClusterLicenseInfo.licenseInfo().getMode()), + License.OperationMode.parse(remoteClusterLicenseInfo.licenseInfo().getMode()), remoteClusterLicenseInfo.clusterAlias(), feature); error.append(message); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index d99dd1ec233..a3343c1850e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -293,6 +293,7 @@ public class XPackClientPlugin extends Plugin implements ActionPlugin, NetworkPl settings.addAll(XPackSettings.getAllSettings()); settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE); + settings.add(LicenseService.ALLOWED_LICENSE_TYPES_SETTING); // we add the `xpack.version` setting to all internal indices settings.add(Setting.simpleString("index.xpack.version", Setting.Property.IndexScope)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java index 21e145546f8..343a520d9b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java @@ -138,7 +138,7 @@ public class TrainedModelConfig implements ToXContentObject, Writeable { throw new IllegalArgumentException("[" + ESTIMATED_OPERATIONS.getPreferredName() + "] must be greater than or equal to 0"); } this.estimatedOperations = estimatedOperations; - this.licenseLevel = License.OperationMode.resolve(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL)); + this.licenseLevel = License.OperationMode.parse(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL)); } public TrainedModelConfig(StreamInput in) throws IOException { @@ -153,7 +153,7 @@ public class TrainedModelConfig implements ToXContentObject, Writeable { input = new TrainedModelInput(in); estimatedHeapMemory = in.readVLong(); estimatedOperations = in.readVLong(); - licenseLevel = License.OperationMode.resolve(in.readString()); + licenseLevel = License.OperationMode.parse(in.readString()); } public String getModelId() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java index c432a207fcb..eb357661d50 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java @@ -34,6 +34,11 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase { licenseService.start(); PlainActionFuture responseFuture = new PlainActionFuture<>(); licenseService.registerLicense(request, responseFuture); + if (responseFuture.isDone()) { + // If the future is done, it means request/license validation failed. + // In which case, this `actionGet` should throw a more useful exception than the verify below. + responseFuture.actionGet(); + } verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class)); } @@ -67,6 +72,11 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase { setInitialState(null, licenseState, settings); licenseService.start(); licenseService.registerLicense(request, responseFuture); + if (responseFuture.isDone()) { + // If the future is done, it means request/license validation failed. + // In which case, this `actionGet` should throw a more useful exception than the verify below. + responseFuture.actionGet(); + } verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class)); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java index 648f48ff2ea..a1fbfbe6c6a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java @@ -57,7 +57,8 @@ public class LicenseOperationModeTests extends ESTestCase { for (String type : types) { try { - OperationMode.resolve(type); + final License.LicenseType licenseType = License.LicenseType.resolve(type); + OperationMode.resolve(licenseType); fail(String.format(Locale.ROOT, "[%s] should not be recognized as an operation mode", type)); } @@ -69,7 +70,8 @@ public class LicenseOperationModeTests extends ESTestCase { private static void assertResolve(OperationMode expected, String... types) { for (String type : types) { - assertThat(OperationMode.resolve(type), equalTo(expected)); + License.LicenseType licenseType = License.LicenseType.resolve(type); + assertThat(OperationMode.resolve(licenseType), equalTo(expected)); } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java index a6933128791..20df885261f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java @@ -34,7 +34,7 @@ public class LicenseOperationModeUpdateTests extends ESTestCase { } public void testLicenseOperationModeUpdate() throws Exception { - String type = randomFrom("trial", "basic", "standard", "gold", "platinum"); + License.LicenseType type = randomFrom(License.LicenseType.values()); License license = License.builder() .uid("id") .expiryDate(0) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java index 750b3d67c5f..08c334fd2e5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java @@ -6,12 +6,47 @@ package org.elasticsearch.license; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.AckedClusterStateUpdateTask; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.licensor.LicenseSigner; +import org.elasticsearch.protocol.xpack.license.LicensesStatus; +import org.elasticsearch.protocol.xpack.license.PutLicenseResponse; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.TestMatchers; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Clock; import java.time.LocalDate; import java.time.ZoneOffset; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.startsWith; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Due to changes in JDK9 where locale data is used from CLDR, the licence message will differ in jdk 8 and jdk9+ @@ -30,4 +65,133 @@ public class LicenseServiceTests extends ESTestCase { assertThat(message, startsWith("License [will expire] on [Thursday, November 15, 2018].\n")); } } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is on its default value (all license types) + */ + public void testRegisterLicenseWithoutTypeRestrictions() throws Exception { + assertRegisterValidLicense(Settings.EMPTY, + randomValueOtherThan(License.LicenseType.BASIC, () -> randomFrom(License.LicenseType.values()))); + } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is set, + * and the uploaded license type matches + */ + public void testSuccessfullyRegisterLicenseMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 1), LicenseService.ALLOWABLE_UPLOAD_TYPES); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + assertRegisterValidLicense(settings, randomFrom(allowed)); + } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES_SETTING} is set, + * and the uploaded license type does not match + */ + public void testFailToRegisterLicenseNotMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 2), LicenseService.ALLOWABLE_UPLOAD_TYPES); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + final License.LicenseType notAllowed = randomValueOtherThanMany( + test -> allowed.contains(test), + () -> randomFrom(LicenseService.ALLOWABLE_UPLOAD_TYPES)); + assertRegisterDisallowedLicenseType(settings, notAllowed); + } + + private void assertRegisterValidLicense(Settings baseSettings, License.LicenseType licenseType) throws IOException { + tryRegisterLicense(baseSettings, licenseType, + future -> assertThat(future.actionGet().status(), equalTo(LicensesStatus.VALID))); + } + + private void assertRegisterDisallowedLicenseType(Settings baseSettings, License.LicenseType licenseType) throws IOException { + tryRegisterLicense(baseSettings, licenseType, future -> { + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, future::actionGet); + assertThat(exception, TestMatchers.throwableWithMessage( + "Registering [" + licenseType.getTypeName() + "] licenses is not allowed on " + "this cluster")); + }); + } + + private void tryRegisterLicense(Settings baseSettings, License.LicenseType licenseType, + Consumer> assertion) throws IOException { + final Settings settings = Settings.builder() + .put(baseSettings) + .put("path.home", createTempDir()) + .put("discovery.type", "single-node") // So we skip TLS checks + .build(); + + final ClusterState clusterState = Mockito.mock(ClusterState.class); + Mockito.when(clusterState.metaData()).thenReturn(MetaData.EMPTY_META_DATA); + + final ClusterService clusterService = Mockito.mock(ClusterService.class); + Mockito.when(clusterService.state()).thenReturn(clusterState); + + final Clock clock = randomBoolean() ? Clock.systemUTC() : Clock.systemDefaultZone(); + final Environment env = TestEnvironment.newEnvironment(settings); + final ResourceWatcherService resourceWatcherService = Mockito.mock(ResourceWatcherService.class); + final XPackLicenseState licenseState = Mockito.mock(XPackLicenseState.class); + final LicenseService service = new LicenseService(settings, clusterService, clock, env, resourceWatcherService, licenseState); + + final PutLicenseRequest request = new PutLicenseRequest(); + request.license(spec(licenseType, TimeValue.timeValueDays(randomLongBetween(1, 1000))), XContentType.JSON); + final PlainActionFuture future = new PlainActionFuture<>(); + service.registerLicense(request, future); + + if (future.isDone()) { + // If validation failed, the future might be done without calling the updater task. + assertion.accept(future); + } else { + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + verify(clusterService, times(1)).submitStateUpdateTask(any(), taskCaptor.capture()); + + final ClusterStateUpdateTask task = taskCaptor.getValue(); + assertThat(task, instanceOf(AckedClusterStateUpdateTask.class)); + ((AckedClusterStateUpdateTask) task).onAllNodesAcked(null); + + assertion.accept(future); + } + } + + private BytesReference spec(License.LicenseType type, TimeValue expires) throws IOException { + final License signed = sign(buildLicense(type, expires)); + return toSpec(signed); + } + + private BytesReference toSpec(License license) throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); + builder.startObject(); + builder.startObject("license"); + license.toInnerXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + builder.endObject(); + builder.flush(); + return BytesReference.bytes(builder); + } + + private License sign(License license) throws IOException { + final Path publicKey = getDataPath("/public.key"); + final Path privateKey = getDataPath("/private.key"); + final LicenseSigner signer = new LicenseSigner(privateKey, publicKey); + + return signer.sign(license); + } + + private License buildLicense(License.LicenseType type, TimeValue expires) { + return License.builder() + .uid(new UUID(randomLong(), randomLong()).toString()) + .type(type) + .expiryDate(System.currentTimeMillis() + expires.millis()) + .issuer(randomAlphaOfLengthBetween(5, 60)) + .issuedTo(randomAlphaOfLengthBetween(5, 60)) + .issueDate(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(randomLongBetween(1, 5000))) + .maxNodes(randomIntBetween(1, 500)) + .signature(null) + .build(); + } }