Skip to content

Commit e3fb15e

Browse files
committed
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. Backport of: elastic#49418
1 parent c732d99 commit e3fb15e

File tree

10 files changed

+273
-20
lines changed

10 files changed

+273
-20
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,23 @@ public static LicenseType parse(String type) throws IllegalArgumentException {
6363
/**
6464
* Backward compatible license type parsing for older license models
6565
*/
66-
public static LicenseType resolve(String name) {
66+
public static LicenseType resolve(License license) {
67+
if (license.version == VERSION_START) {
68+
// in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum
69+
return resolve(license.subscriptionType);
70+
} else {
71+
// in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum
72+
// in 5.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum
73+
// in 6.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum
74+
// in 7.x: the acceptable values for 'type': trial | basic | standard | dev | gold | platinum | enterprise
75+
return resolve(license.type);
76+
}
77+
}
78+
79+
/**
80+
* Backward compatible license type parsing for older license models
81+
*/
82+
static LicenseType resolve(String name) {
6783
switch (name.toLowerCase(Locale.ROOT)) {
6884
case "missing":
6985
return null;
@@ -171,8 +187,12 @@ public static int compare(OperationMode opMode1, OperationMode opMode2) {
171187
return Integer.compare(opMode1.id, opMode2.id);
172188
}
173189

174-
public static OperationMode resolve(String typeName) {
175-
LicenseType type = LicenseType.resolve(typeName);
190+
/**
191+
* Determine the operating mode for a license type
192+
* @see LicenseType#resolve(License)
193+
* @see #parse(String)
194+
*/
195+
public static OperationMode resolve(LicenseType type) {
176196
if (type == null) {
177197
return MISSING;
178198
}
@@ -193,6 +213,21 @@ public static OperationMode resolve(String typeName) {
193213
}
194214
}
195215

216+
/**
217+
* Parses an {@code OperatingMode} from a String.
218+
* The string must name an operating mode, and not a licensing level (that is, it cannot parse old style license levels
219+
* such as "dev" or "silver").
220+
* @see #description()
221+
*/
222+
public static OperationMode parse(String mode) {
223+
try {
224+
return OperationMode.valueOf(mode.toUpperCase(Locale.ROOT));
225+
} catch (IllegalArgumentException e) {
226+
throw new IllegalArgumentException("unrecognised license operating mode [ " + mode + "], supported modes are ["
227+
+ Stream.of(values()).map(OperationMode::description).collect(Collectors.joining(",")) + "]");
228+
}
229+
}
230+
196231
public String description() {
197232
return name().toLowerCase(Locale.ROOT);
198233
}
@@ -218,13 +253,7 @@ private License(int version, String uid, String issuer, String issuedTo, long is
218253
}
219254
this.maxNodes = maxNodes;
220255
this.startDate = startDate;
221-
if (version == VERSION_START) {
222-
// in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum
223-
this.operationMode = OperationMode.resolve(subscriptionType);
224-
} else {
225-
// in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum
226-
this.operationMode = OperationMode.resolve(type);
227-
}
256+
this.operationMode = OperationMode.resolve(LicenseType.resolve(this));
228257
validate();
229258
}
230259

x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.Set;
4949
import java.util.concurrent.atomic.AtomicReference;
5050
import java.util.stream.Collectors;
51+
import java.util.stream.Stream;
5152

5253
/**
5354
* Service responsible for managing {@link LicensesMetaData}.
@@ -65,6 +66,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
6566
return SelfGeneratedLicense.validateSelfGeneratedType(type);
6667
}, Setting.Property.NodeScope);
6768

69+
static final List<License.LicenseType> ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes();
70+
71+
public static final Setting<List<License.LicenseType>> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting("xpack.license.upload.types",
72+
Collections.unmodifiableList(ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.toList())),
73+
License.LicenseType::parse, LicenseService::validateUploadTypesSetting, Setting.Property.NodeScope);
74+
6875
// pkg private for tests
6976
static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24);
7077

@@ -105,6 +112,12 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
105112
*/
106113
private List<ExpirationCallback> expirationCallbacks = new ArrayList<>();
107114

115+
/**
116+
* Which license types are permitted to be uploaded to the cluster
117+
* @see #ALLOWED_LICENSE_TYPES_SETTING
118+
*/
119+
private final List<License.LicenseType> allowedLicenseTypes;
120+
108121
/**
109122
* Max number of nodes licensed by generated trial license
110123
*/
@@ -124,6 +137,7 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl
124137
this.clock = clock;
125138
this.scheduler = new SchedulerEngine(settings, clock);
126139
this.licenseState = licenseState;
140+
this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES_SETTING.get(settings);
127141
this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService,
128142
XPackPlugin.resolveConfigFile(env, "license_mode"), logger,
129143
() -> updateLicenseState(getLicensesMetaData()));
@@ -197,8 +211,20 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene
197211
final long now = clock.millis();
198212
if (!LicenseVerifier.verifyLicense(newLicense) || newLicense.issueDate() > now || newLicense.startDate() > now) {
199213
listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID));
200-
} else if (newLicense.type().equals(License.LicenseType.BASIC.getTypeName())) {
214+
return;
215+
}
216+
final License.LicenseType licenseType;
217+
try {
218+
licenseType = License.LicenseType.resolve(newLicense);
219+
} catch (Exception e) {
220+
listener.onFailure(e);
221+
return;
222+
}
223+
if (licenseType == License.LicenseType.BASIC) {
201224
listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed."));
225+
} else if (isAllowedLicenseType(licenseType) == false) {
226+
listener.onFailure(new IllegalArgumentException(
227+
"Registering [" + licenseType.getTypeName() + "] licenses is not allowed on this cluster"));
202228
} else if (newLicense.expiryDate() < now) {
203229
listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED));
204230
} else {
@@ -273,6 +299,11 @@ private static boolean licenseIsCompatible(License license, Version version) {
273299
}
274300
}
275301

302+
private boolean isAllowedLicenseType(License.LicenseType type) {
303+
logger.debug("Checking license [{}] against allowed license types: {}", type, allowedLicenseTypes);
304+
return allowedLicenseTypes.contains(type);
305+
}
306+
276307
public static Map<String, String[]> getAckMessages(License newLicense, License currentLicense) {
277308
Map<String, String[]> acknowledgeMessages = new HashMap<>();
278309
if (!License.isAutoGeneratedLicense(currentLicense.signature()) // current license is not auto-generated
@@ -575,4 +606,20 @@ private static boolean isProductionMode(Settings settings, DiscoveryNode localNo
575606
private static boolean isBoundToLoopback(DiscoveryNode localNode) {
576607
return localNode.getAddress().address().getAddress().isLoopbackAddress();
577608
}
609+
610+
private static List<License.LicenseType> getAllowableUploadTypes() {
611+
return Collections.unmodifiableList(Stream.of(License.LicenseType.values())
612+
.filter(t -> t != License.LicenseType.BASIC)
613+
.collect(Collectors.toList()));
614+
}
615+
616+
private static void validateUploadTypesSetting(List<License.LicenseType> value) {
617+
if (ALLOWABLE_UPLOAD_TYPES.containsAll(value) == false) {
618+
throw new IllegalArgumentException("Invalid value [" +
619+
value.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) +
620+
"] for " + ALLOWED_LICENSE_TYPES_SETTING.getKey() + ", allowed values are [" +
621+
ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) +
622+
"]");
623+
}
624+
}
578625
}

x-pack/plugin/core/src/main/java/org/elasticsearch/license/OperationModeFileWatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private synchronized void onChange(Path file) {
106106
// this UTF-8 conversion is much pickier than java String
107107
final String operationMode = new BytesRef(content).utf8ToString();
108108
try {
109-
newOperationMode = OperationMode.resolve(operationMode);
109+
newOperationMode = OperationMode.parse(operationMode);
110110
} catch (IllegalArgumentException e) {
111111
logger.error(
112112
(Supplier<?>) () -> new ParameterizedMessage(

x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public RemoteClusterLicenseChecker(final Client client, final Predicate<License.
138138
}
139139

140140
public static boolean isLicensePlatinumOrTrial(final XPackInfoResponse.LicenseInfo licenseInfo) {
141-
final License.OperationMode mode = License.OperationMode.resolve(licenseInfo.getMode());
141+
final License.OperationMode mode = License.OperationMode.parse(licenseInfo.getMode());
142142
return mode == License.OperationMode.PLATINUM || mode == License.OperationMode.TRIAL;
143143
}
144144

@@ -168,7 +168,7 @@ public void onResponse(final XPackInfoResponse xPackInfoResponse) {
168168
return;
169169
}
170170
if ((licenseInfo.getStatus() == LicenseStatus.ACTIVE) == false
171-
|| predicate.test(License.OperationMode.resolve(licenseInfo.getMode())) == false) {
171+
|| predicate.test(License.OperationMode.parse(licenseInfo.getMode())) == false) {
172172
listener.onResponse(LicenseCheck.failure(new RemoteClusterLicenseInfo(clusterAlias.get(), licenseInfo)));
173173
return;
174174
}
@@ -282,7 +282,7 @@ public static String buildErrorMessage(
282282
final String message = String.format(
283283
Locale.ROOT,
284284
"the license mode [%s] on cluster [%s] does not enable [%s]",
285-
License.OperationMode.resolve(remoteClusterLicenseInfo.licenseInfo().getMode()),
285+
License.OperationMode.parse(remoteClusterLicenseInfo.licenseInfo().getMode()),
286286
remoteClusterLicenseInfo.clusterAlias(),
287287
feature);
288288
error.append(message);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ public List<Setting<?>> getSettings() {
293293
settings.addAll(XPackSettings.getAllSettings());
294294

295295
settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE);
296+
settings.add(LicenseService.ALLOWED_LICENSE_TYPES_SETTING);
296297

297298
// we add the `xpack.version` setting to all internal indices
298299
settings.add(Setting.simpleString("index.xpack.version", Setting.Property.IndexScope));

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/TrainedModelConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public static TrainedModelConfig.Builder fromXContent(XContentParser parser, boo
138138
throw new IllegalArgumentException("[" + ESTIMATED_OPERATIONS.getPreferredName() + "] must be greater than or equal to 0");
139139
}
140140
this.estimatedOperations = estimatedOperations;
141-
this.licenseLevel = License.OperationMode.resolve(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL));
141+
this.licenseLevel = License.OperationMode.parse(ExceptionsHelper.requireNonNull(licenseLevel, LICENSE_LEVEL));
142142
}
143143

144144
public TrainedModelConfig(StreamInput in) throws IOException {
@@ -153,7 +153,7 @@ public TrainedModelConfig(StreamInput in) throws IOException {
153153
input = new TrainedModelInput(in);
154154
estimatedHeapMemory = in.readVLong();
155155
estimatedOperations = in.readVLong();
156-
licenseLevel = License.OperationMode.resolve(in.readString());
156+
licenseLevel = License.OperationMode.parse(in.readString());
157157
}
158158

159159
public String getModelId() {

x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public void testFIPSCheckWithAllowedLicense() throws Exception {
3434
licenseService.start();
3535
PlainActionFuture<PutLicenseResponse> responseFuture = new PlainActionFuture<>();
3636
licenseService.registerLicense(request, responseFuture);
37+
if (responseFuture.isDone()) {
38+
// If the future is done, it means request/license validation failed.
39+
// In which case, this `actionGet` should throw a more useful exception than the verify below.
40+
responseFuture.actionGet();
41+
}
3742
verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class));
3843
}
3944

@@ -67,6 +72,11 @@ public void testFIPSCheckWithoutAllowedLicense() throws Exception {
6772
setInitialState(null, licenseState, settings);
6873
licenseService.start();
6974
licenseService.registerLicense(request, responseFuture);
75+
if (responseFuture.isDone()) {
76+
// If the future is done, it means request/license validation failed.
77+
// In which case, this `actionGet` should throw a more useful exception than the verify below.
78+
responseFuture.actionGet();
79+
}
7080
verify(clusterService).submitStateUpdateTask(any(String.class), any(ClusterStateUpdateTask.class));
7181
}
7282
}

x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public void testResolveUnknown() {
5757

5858
for (String type : types) {
5959
try {
60-
OperationMode.resolve(type);
60+
final License.LicenseType licenseType = License.LicenseType.resolve(type);
61+
OperationMode.resolve(licenseType);
6162

6263
fail(String.format(Locale.ROOT, "[%s] should not be recognized as an operation mode", type));
6364
}
@@ -69,7 +70,8 @@ public void testResolveUnknown() {
6970

7071
private static void assertResolve(OperationMode expected, String... types) {
7172
for (String type : types) {
72-
assertThat(OperationMode.resolve(type), equalTo(expected));
73+
License.LicenseType licenseType = License.LicenseType.resolve(type);
74+
assertThat(OperationMode.resolve(licenseType), equalTo(expected));
7375
}
7476
}
7577
}

x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void init() throws Exception {
3434
}
3535

3636
public void testLicenseOperationModeUpdate() throws Exception {
37-
String type = randomFrom("trial", "basic", "standard", "gold", "platinum");
37+
License.LicenseType type = randomFrom(License.LicenseType.values());
3838
License license = License.builder()
3939
.uid("id")
4040
.expiryDate(0)

0 commit comments

Comments
 (0)