Skip to content

Commit

Permalink
Add setting to restrict license types (elastic#49418)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tvernum authored and SivagurunathanV committed Jan 21, 2020
1 parent 1f8c96a commit d12c7cb
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,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;
Expand Down Expand Up @@ -165,8 +181,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;
}
Expand All @@ -187,6 +207,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);
}
Expand All @@ -212,13 +247,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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -64,6 +65,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",
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);

Expand Down Expand Up @@ -104,6 +111,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
*/
Expand All @@ -123,6 +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_SETTING.get(settings);
this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService,
XPackPlugin.resolveConfigFile(env, "license_mode"), logger,
() -> updateLicenseState(getLicensesMetaData()));
Expand Down Expand Up @@ -196,8 +210,20 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene
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 {
Expand Down Expand Up @@ -272,6 +298,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<String, String[]> getAckMessages(License newLicense, License currentLicense) {
Map<String, String[]> acknowledgeMessages = new HashMap<>();
if (!License.isAutoGeneratedLicense(currentLicense.signature()) // current license is not auto-generated
Expand Down Expand Up @@ -574,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<License.LicenseType> getAllowableUploadTypes() {
return Stream.of(License.LicenseType.values())
.filter(t -> t != License.LicenseType.BASIC)
.collect(Collectors.toUnmodifiableList());
}

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(",")) +
"]");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public RemoteClusterLicenseChecker(final Client client, final Predicate<License.
}

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

Expand Down Expand Up @@ -168,7 +168,7 @@ public void onResponse(final XPackInfoResponse xPackInfoResponse) {
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;
}
Expand Down Expand Up @@ -282,7 +282,7 @@ public static String buildErrorMessage(
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ public List<Setting<?>> getSettings() {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public static TrainedModelConfig.Builder fromXContent(XContentParser parser, boo
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 {
Expand All @@ -153,7 +153,7 @@ public TrainedModelConfig(StreamInput in) throws IOException {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public void testFIPSCheckWithAllowedLicense() throws Exception {
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));
}

Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public void testResolveUnknown() {

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));
}
Expand All @@ -69,7 +70,8 @@ public void testResolveUnknown() {

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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void init() throws Exception {
}

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)
Expand Down
Loading

0 comments on commit d12c7cb

Please sign in to comment.