From 0d178d8739ef7a74ef3c90780e739a3ed5c2a2aa Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 18 Nov 2019 17:17:10 +1100 Subject: [PATCH 1/3] Add setting to restrict license types 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. --- .../org/elasticsearch/license/License.java | 3 + .../elasticsearch/license/LicenseService.java | 27 ++- .../xpack/core/XPackClientPlugin.java | 1 + .../license/LicenseFIPSTests.java | 10 ++ .../license/LicenseServiceTests.java | 162 ++++++++++++++++++ .../org/elasticsearch/license/TestUtils.java | 4 +- 6 files changed, 205 insertions(+), 2 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 6731518f5b534..51389535d0671 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 @@ -47,6 +47,9 @@ public enum LicenseType { ENTERPRISE, TRIAL; + static final List ALL_TYPE_NAMES = + Stream.of(values()).map(LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); + public String getTypeName() { return name().toLowerCase(Locale.ROOT); } 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 f16cb2fbe3932..c9429610f6a01 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 @@ -64,6 +64,9 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste return SelfGeneratedLicense.validateSelfGeneratedType(type); }, Setting.Property.NodeScope); + public static final Setting> ALLOWED_LICENSE_TYPES = Setting.listSetting("xpack.license.upload.types", + License.LicenseType.ALL_TYPE_NAMES, License.LicenseType::parse, Setting.Property.NodeScope); + // pkg private for tests static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); @@ -104,6 +107,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 + */ + private final List allowedLicenseTypes; + /** * Max number of nodes licensed by generated trial license */ @@ -123,6 +132,7 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl this.clock = clock; this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; + this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES.get(settings); this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, () -> updateLicenseState(getLicensesMetaData())); @@ -193,11 +203,21 @@ public void on(License license) { */ public void registerLicense(final PutLicenseRequest request, final ActionListener listener) { final License newLicense = request.license(); + final License.LicenseType licenseType; + try { + licenseType = License.LicenseType.resolve(newLicense.type()); + } catch (Exception e) { + listener.onFailure(e); + return; + } 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())) { + } else 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 { @@ -272,6 +292,11 @@ private static boolean licenseIsCompatible(License license, Version version) { } } + 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 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 ec4d1cdce0fed..9baada5b703e1 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 @@ -274,6 +274,7 @@ public List> getSettings() { settings.addAll(XPackSettings.getAllSettings()); settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE); + settings.add(LicenseService.ALLOWED_LICENSE_TYPES); // 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/test/java/org/elasticsearch/license/LicenseFIPSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java index c432a207fcb70..eb357661d50ca 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 void testFIPSCheckWithAllowedLicense() throws Exception { 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 void testFIPSCheckWithoutAllowedLicense() throws Exception { 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/LicenseServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java index 57394e647c9ea..16f4c4dde63f3 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,13 +6,48 @@ package org.elasticsearch.license; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.bootstrap.JavaVersion; +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+ @@ -33,4 +68,131 @@ public void testLogExpirationWarning() { assertThat(message, startsWith("License [will expire] on [Thursday, November 15, 2018].\n")); } } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES} 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} is set, and the uploaded license type matches + */ + public void testSuccessfullyRegisterLicenseMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(2, License.LicenseType.values().length - 1), License.LicenseType.values()); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + assertRegisterValidLicense(settings, randomValueOtherThan(License.LicenseType.BASIC, () -> randomFrom(allowed))); + } + + /** + * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES} is set, and the uploaded license type does not match + */ + public void testFailToRegisterLicenseNotMatchingTypeRestrictions() throws Exception { + final List allowed = randomSubsetOf( + randomIntBetween(1, License.LicenseType.values().length - 3), License.LicenseType.values()); + final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); + final Settings settings = Settings.builder() + .putList("xpack.license.upload.types", allowedNames) + .build(); + final License.LicenseType notAllowed = randomValueOtherThanMany( + test -> test == License.LicenseType.BASIC || allowed.contains(test), + () -> randomFrom(License.LicenseType.values())); + 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(); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java index 47dad7e18eb32..35d4fe1c7e69b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java @@ -241,7 +241,9 @@ public static License generateSignedLicense(long issueDate, TimeValue expiryDura } public static License generateSignedLicense(String type, long issueDate, TimeValue expiryDuration) throws Exception { - return generateSignedLicense(type, randomIntBetween(License.VERSION_START, License.VERSION_CURRENT), issueDate, expiryDuration); + // If there's an explicit "type", don't generate v1 style license because they don't have the same types. + int version = randomIntBetween(type == null ? License.VERSION_START : License.VERSION_NO_FEATURE_TYPE, License.VERSION_CURRENT); + return generateSignedLicense(type, version, issueDate, expiryDuration); } public static License generateSignedLicenseOldSignature() { From b08cde5784692de0420a1748e5f598df2e1903fe Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 22 Nov 2019 12:02:54 +1100 Subject: [PATCH 2/3] Fix resolution of v1 style licenses --- .../org/elasticsearch/license/License.java | 49 +++++++++++++++---- .../elasticsearch/license/LicenseService.java | 2 +- .../license/OperationModeFileWatcher.java | 2 +- .../license/RemoteClusterLicenseChecker.java | 6 +-- .../core/ml/inference/TrainedModelConfig.java | 4 +- .../license/LicenseOperationModeTests.java | 6 ++- .../LicenseOperationModeUpdateTests.java | 2 +- .../org/elasticsearch/license/TestUtils.java | 4 +- 8 files changed, 52 insertions(+), 23 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 51389535d0671..edfb387790285 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 @@ -66,7 +66,23 @@ public static LicenseType parse(String type) throws IllegalArgumentException { /** * 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; @@ -168,8 +184,12 @@ public static int compare(OperationMode opMode1, OperationMode opMode2) { 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; } @@ -190,6 +210,21 @@ public static OperationMode resolve(String typeName) { } } + /** + * 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); } @@ -215,13 +250,7 @@ private License(int version, String uid, String issuer, String issuedTo, long is } 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 c9429610f6a01..197055f01ff45 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 @@ -205,7 +205,7 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene final License newLicense = request.license(); final License.LicenseType licenseType; try { - licenseType = License.LicenseType.resolve(newLicense.type()); + licenseType = License.LicenseType.resolve(newLicense); } catch (Exception e) { listener.onFailure(e); return; 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 b8e6446b9f49f..ee08b9f7330cf 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 @@ private synchronized void onChange(Path file) { // 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 7d5a3b5e9a53d..5de1186767f4b 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 RemoteClusterLicenseChecker(final Client client, final Predicate Date: Fri, 29 Nov 2019 15:57:33 +1100 Subject: [PATCH 3/3] Address feedback --- .../org/elasticsearch/license/License.java | 3 -- .../elasticsearch/license/LicenseService.java | 38 +++++++++++++++---- .../xpack/core/XPackClientPlugin.java | 2 +- .../license/LicenseServiceTests.java | 19 +++++----- 4 files changed, 41 insertions(+), 21 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 edfb387790285..004c9ff987764 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 @@ -47,9 +47,6 @@ public enum LicenseType { ENTERPRISE, TRIAL; - static final List ALL_TYPE_NAMES = - Stream.of(values()).map(LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); - public String getTypeName() { return name().toLowerCase(Locale.ROOT); } 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 197055f01ff45..af34d31c14422 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 @@ -47,6 +47,7 @@ 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}. @@ -64,8 +65,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste return SelfGeneratedLicense.validateSelfGeneratedType(type); }, Setting.Property.NodeScope); - public static final Setting> ALLOWED_LICENSE_TYPES = Setting.listSetting("xpack.license.upload.types", - License.LicenseType.ALL_TYPE_NAMES, License.LicenseType::parse, Setting.Property.NodeScope); + static final List ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes(); + + public static final Setting> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting("xpack.license.upload.types", + ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()), + 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); @@ -109,7 +113,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste /** * Which license types are permitted to be uploaded to the cluster - * @see #ALLOWED_LICENSE_TYPES + * @see #ALLOWED_LICENSE_TYPES_SETTING */ private final List allowedLicenseTypes; @@ -132,7 +136,7 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl this.clock = clock; this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; - this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES.get(settings); + this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES_SETTING.get(settings); this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, () -> updateLicenseState(getLicensesMetaData())); @@ -203,6 +207,11 @@ public void on(License license) { */ public void registerLicense(final PutLicenseRequest request, final ActionListener listener) { final License newLicense = request.license(); + final long now = clock.millis(); + if (!LicenseVerifier.verifyLicense(newLicense) || newLicense.issueDate() > now || newLicense.startDate() > now) { + listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID)); + return; + } final License.LicenseType licenseType; try { licenseType = License.LicenseType.resolve(newLicense); @@ -210,10 +219,7 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene listener.onFailure(e); return; } - final long now = clock.millis(); - if (!LicenseVerifier.verifyLicense(newLicense) || newLicense.issueDate() > now || newLicense.startDate() > now) { - listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID)); - } else if (licenseType == License.LicenseType.BASIC) { + if (licenseType == License.LicenseType.BASIC) { listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed.")); } else if (isAllowedLicenseType(licenseType) == false) { listener.onFailure(new IllegalArgumentException( @@ -599,4 +605,20 @@ private static boolean isProductionMode(Settings settings, DiscoveryNode localNo private static boolean isBoundToLoopback(DiscoveryNode localNode) { return localNode.getAddress().address().getAddress().isLoopbackAddress(); } + + private static List getAllowableUploadTypes() { + return Stream.of(License.LicenseType.values()) + .filter(t -> t != License.LicenseType.BASIC) + .collect(Collectors.toUnmodifiableList()); + } + + 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/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index 15a4a513134c1..9128f75a0937b 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 @@ -267,7 +267,7 @@ public List> getSettings() { settings.addAll(XPackSettings.getAllSettings()); settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE); - settings.add(LicenseService.ALLOWED_LICENSE_TYPES); + 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/test/java/org/elasticsearch/license/LicenseServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java index 0aeee80b62b35..b1b22f15c259f 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 @@ -7,7 +7,6 @@ import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.bootstrap.JavaVersion; import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; @@ -68,7 +67,7 @@ public void testLogExpirationWarning() { } /** - * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES} is on its default value (all license types) + * 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, @@ -76,31 +75,33 @@ public void testRegisterLicenseWithoutTypeRestrictions() throws Exception { } /** - * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES} is set, and the uploaded license type matches + * 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(2, License.LicenseType.values().length - 1), License.LicenseType.values()); + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 1), LicenseService.ALLOWABLE_UPLOAD_TYPES); final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); final Settings settings = Settings.builder() .putList("xpack.license.upload.types", allowedNames) .build(); - assertRegisterValidLicense(settings, randomValueOtherThan(License.LicenseType.BASIC, () -> randomFrom(allowed))); + assertRegisterValidLicense(settings, randomFrom(allowed)); } /** - * Tests loading a license when {@link LicenseService#ALLOWED_LICENSE_TYPES} is set, and the uploaded license type does not match + * 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, License.LicenseType.values().length - 3), License.LicenseType.values()); + randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 2), LicenseService.ALLOWABLE_UPLOAD_TYPES); final List allowedNames = allowed.stream().map(License.LicenseType::getTypeName).collect(Collectors.toUnmodifiableList()); final Settings settings = Settings.builder() .putList("xpack.license.upload.types", allowedNames) .build(); final License.LicenseType notAllowed = randomValueOtherThanMany( - test -> test == License.LicenseType.BASIC || allowed.contains(test), - () -> randomFrom(License.LicenseType.values())); + test -> allowed.contains(test), + () -> randomFrom(LicenseService.ALLOWABLE_UPLOAD_TYPES)); assertRegisterDisallowedLicenseType(settings, notAllowed); }