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
This commit is contained in:
Tim Vernum 2019-12-17 14:58:58 +11:00 committed by GitHub
parent 0efb241b3c
commit ce2aab3f2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 273 additions and 20 deletions

View File

@ -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();
}

View File

@ -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<License.LicenseType> ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes();
public static final Setting<List<License.LicenseType>> 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<ExpirationCallback> expirationCallbacks = new ArrayList<>();
/**
* Which license types are permitted to be uploaded to the cluster
* @see #ALLOWED_LICENSE_TYPES_SETTING
*/
private final List<License.LicenseType> 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<String, String[]> getAckMessages(License newLicense, License currentLicense) {
Map<String, String[]> 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<License.LicenseType> getAllowableUploadTypes() {
return Collections.unmodifiableList(Stream.of(License.LicenseType.values())
.filter(t -> t != License.LicenseType.BASIC)
.collect(Collectors.toList()));
}
private static void validateUploadTypesSetting(List<License.LicenseType> 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(",")) +
"]");
}
}
}

View File

@ -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(

View File

@ -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);

View File

@ -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));

View File

@ -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() {

View File

@ -34,6 +34,11 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase {
licenseService.start();
PlainActionFuture<PutLicenseResponse> 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));
}
}

View File

@ -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));
}
}
}

View File

@ -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)

View File

@ -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<License.LicenseType> allowed = randomSubsetOf(
randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 1), LicenseService.ALLOWABLE_UPLOAD_TYPES);
final List<String> 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<License.LicenseType> allowed = randomSubsetOf(
randomIntBetween(1, LicenseService.ALLOWABLE_UPLOAD_TYPES.size() - 2), LicenseService.ALLOWABLE_UPLOAD_TYPES);
final List<String> 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<PlainActionFuture<PutLicenseResponse>> 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<PutLicenseResponse> 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<ClusterStateUpdateTask> 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();
}
}