Skip to content

Commit a6351d6

Browse files
authored
Add setting to restrict license types (#49418)
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.
1 parent 12e1bc4 commit a6351d6

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;
@@ -165,8 +181,12 @@ public static int compare(OperationMode opMode1, OperationMode opMode2) {
165181
return Integer.compare(opMode1.id, opMode2.id);
166182
}
167183

168-
public static OperationMode resolve(String typeName) {
169-
LicenseType type = LicenseType.resolve(typeName);
184+
/**
185+
* Determine the operating mode for a license type
186+
* @see LicenseType#resolve(License)
187+
* @see #parse(String)
188+
*/
189+
public static OperationMode resolve(LicenseType type) {
170190
if (type == null) {
171191
return MISSING;
172192
}
@@ -187,6 +207,21 @@ public static OperationMode resolve(String typeName) {
187207
}
188208
}
189209

210+
/**
211+
* Parses an {@code OperatingMode} from a String.
212+
* The string must name an operating mode, and not a licensing level (that is, it cannot parse old style license levels
213+
* such as "dev" or "silver").
214+
* @see #description()
215+
*/
216+
public static OperationMode parse(String mode) {
217+
try {
218+
return OperationMode.valueOf(mode.toUpperCase(Locale.ROOT));
219+
} catch (IllegalArgumentException e) {
220+
throw new IllegalArgumentException("unrecognised license operating mode [ " + mode + "], supported modes are ["
221+
+ Stream.of(values()).map(OperationMode::description).collect(Collectors.joining(",")) + "]");
222+
}
223+
}
224+
190225
public String description() {
191226
return name().toLowerCase(Locale.ROOT);
192227
}
@@ -212,13 +247,7 @@ private License(int version, String uid, String issuer, String issuedTo, long is
212247
}
213248
this.maxNodes = maxNodes;
214249
this.startDate = startDate;
215-
if (version == VERSION_START) {
216-
// in 1.x: the acceptable values for 'subscription_type': none | dev | silver | gold | platinum
217-
this.operationMode = OperationMode.resolve(subscriptionType);
218-
} else {
219-
// in 2.x: the acceptable values for 'type': trial | basic | silver | dev | gold | platinum
220-
this.operationMode = OperationMode.resolve(type);
221-
}
250+
this.operationMode = OperationMode.resolve(LicenseType.resolve(this));
222251
validate();
223252
}
224253

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
@@ -47,6 +47,7 @@
4747
import java.util.Set;
4848
import java.util.concurrent.atomic.AtomicReference;
4949
import java.util.stream.Collectors;
50+
import java.util.stream.Stream;
5051

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

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

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

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

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

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
@@ -267,6 +267,7 @@ public List<Setting<?>> getSettings() {
267267
settings.addAll(XPackSettings.getAllSettings());
268268

269269
settings.add(LicenseService.SELF_GENERATED_LICENSE_TYPE);
270+
settings.add(LicenseService.ALLOWED_LICENSE_TYPES_SETTING);
270271

271272
// we add the `xpack.version` setting to all internal indices
272273
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)