Przeglądaj źródła

Support "enterprise" license types (#49223)

This adds "enterprise" as an acceptable type for a license loaded
through the PUT _license API.

Internally an enterprise license is treated as having a "platinum"
operating mode.

The handling of License types was refactored to have a new explicit
"LicenseType" enum in addition to the existing "OperatingMode" enum.
Tim Vernum 6 lat temu
rodzic
commit
31512fb6ec

+ 3 - 0
x-pack/license-tools/src/main/java/org/elasticsearch/license/licensor/tools/LicenseGeneratorTool.java

@@ -88,6 +88,9 @@ public class LicenseGeneratorTool extends LoggingAwareCommand {
                     ExitCodes.USAGE,
                     "Must specify either --license or --licenseFile");
         }
+        if (licenseSpec == null) {
+            throw new UserException(ExitCodes.DATA_ERROR, "Could not parse license spec");
+        }
 
         // sign
         License license = new LicenseSigner(privateKeyPath, publicKeyPath).sign(licenseSpec);

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

@@ -5,15 +5,6 @@
  */
 package org.elasticsearch.license;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
-
 import org.apache.lucene.util.CollectionUtil;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ElasticsearchParseException;
@@ -31,11 +22,83 @@ import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.protocol.xpack.license.LicenseStatus;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 /**
  * Data structure for license. Use {@link Builder} to build a license.
  * Provides serialization/deserialization & validation methods for license object
  */
 public class License implements ToXContentObject {
+
+    public enum LicenseType {
+        BASIC,
+        STANDARD,
+        GOLD,
+        PLATINUM,
+        ENTERPRISE,
+        TRIAL;
+
+        public String getTypeName() {
+            return name().toLowerCase(Locale.ROOT);
+        }
+
+        public static LicenseType parse(String type) throws IllegalArgumentException {
+            try {
+                return LicenseType.valueOf(type.toUpperCase(Locale.ROOT));
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException("unrecognised license type [ " + type + "], supported license types are ["
+                    + Stream.of(values()).map(LicenseType::getTypeName).collect(Collectors.joining(",")) + "]");
+            }
+        }
+
+        /**
+         * Backward compatible license type parsing for older license models
+         */
+        public static LicenseType resolve(String name) {
+            switch (name.toLowerCase(Locale.ROOT)) {
+                case "missing":
+                    return null;
+                case "trial":
+                case "none": // bwc for 1.x subscription_type field
+                case "dev": // bwc for 1.x subscription_type field
+                case "development": // bwc for 1.x subscription_type field
+                    return TRIAL;
+                case "basic":
+                    return BASIC;
+                case "standard":
+                    return STANDARD;
+                case "silver":
+                case "gold":
+                    return GOLD;
+                case "platinum":
+                case "cloud_internal":
+                case "internal": // bwc for 1.x subscription_type field
+                    return PLATINUM;
+                case "enterprise":
+                    return ENTERPRISE;
+                default:
+                    throw new IllegalArgumentException("unknown license type [" + name + "]");
+            }
+        }
+
+        static boolean isBasic(String typeName) {
+            return BASIC.getTypeName().equals(typeName);
+        }
+
+        static boolean isTrial(String typeName) {
+            return TRIAL.getTypeName().equals(typeName);
+        }
+    }
+
     public static final int VERSION_START = 1;
     public static final int VERSION_NO_FEATURE_TYPE = 2;
     public static final int VERSION_START_DATE = 3;
@@ -102,28 +165,25 @@ public class License implements ToXContentObject {
             return Integer.compare(opMode1.id, opMode2.id);
         }
 
-        public static OperationMode resolve(String type) {
-            switch (type.toLowerCase(Locale.ROOT)) {
-                case "missing":
-                    return MISSING;
-                case "trial":
-                case "none": // bwc for 1.x subscription_type field
-                case "dev": // bwc for 1.x subscription_type field
-                case "development": // bwc for 1.x subscription_type field
-                    return TRIAL;
-                case "basic":
+        public static OperationMode resolve(String typeName) {
+            LicenseType type = LicenseType.resolve(typeName);
+            if (type == null) {
+                return MISSING;
+            }
+            switch (type) {
+                case BASIC:
                     return BASIC;
-                case "standard":
+                case STANDARD:
                     return STANDARD;
-                case "silver":
-                case "gold":
+                case GOLD:
                     return GOLD;
-                case "platinum":
-                case "cloud_internal":
-                case "internal": // bwc for 1.x subscription_type field
+                case PLATINUM:
+                case ENTERPRISE: // TODO Add an explicit enterprise operating mode
                     return PLATINUM;
+                case TRIAL:
+                    return TRIAL;
                 default:
-                    throw new IllegalArgumentException("unknown type [" + type + "]");
+                    throw new IllegalArgumentException("unsupported license type [" + type.getTypeName() + "]");
             }
         }
 
@@ -301,7 +361,7 @@ public class License implements ToXContentObject {
             throw new IllegalStateException("maxNodes has to be set");
         } else if (expiryDate == -1) {
             throw new IllegalStateException("expiryDate has to be set");
-        } else if (expiryDate == LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS && "basic".equals(type) == false) {
+        } else if (expiryDate == LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS && LicenseType.isBasic(type) == false) {
             throw new IllegalStateException("only basic licenses are allowed to have no expiration");
         }
     }
@@ -689,6 +749,10 @@ public class License implements ToXContentObject {
             return this;
         }
 
+        public Builder type(LicenseType type) {
+            return type(type.getTypeName());
+        }
+
         public Builder type(String type) {
             this.type = type;
             return this;
@@ -778,6 +842,7 @@ public class License implements ToXContentObject {
             }
             return this;
         }
+
     }
 
 }

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

@@ -39,15 +39,14 @@ import org.elasticsearch.xpack.core.scheduler.SchedulerEngine;
 
 import java.time.Clock;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 /**
  * Service responsible for managing {@link LicensesMetaData}.
@@ -59,19 +58,17 @@ import java.util.concurrent.atomic.AtomicReference;
 public class LicenseService extends AbstractLifecycleComponent implements ClusterStateListener, SchedulerEngine.Listener {
     private static final Logger logger = LogManager.getLogger(LicenseService.class);
 
-    public static final Setting<String> SELF_GENERATED_LICENSE_TYPE = new Setting<>("xpack.license.self_generated.type",
-            (s) -> "basic", (s) -> {
-        if (SelfGeneratedLicense.validSelfGeneratedType(s)) {
-            return s;
-        } else {
-            throw new IllegalArgumentException("Illegal self generated license type [" + s + "]. Must be trial or basic.");
-        }
+    public static final Setting<License.LicenseType> SELF_GENERATED_LICENSE_TYPE = new Setting<>("xpack.license.self_generated.type",
+        (s) -> License.LicenseType.BASIC.getTypeName(), (s) -> {
+        final License.LicenseType type = License.LicenseType.parse(s);
+        return SelfGeneratedLicense.validateSelfGeneratedType(type);
     }, Setting.Property.NodeScope);
 
     // pkg private for tests
     static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24);
 
-    static final Set<String> VALID_TRIAL_TYPES = new HashSet<>(Arrays.asList("trial", "platinum", "gold"));
+    static final Set<License.LicenseType> VALID_TRIAL_TYPES = Set.of(
+        License.LicenseType.GOLD, License.LicenseType.PLATINUM, License.LicenseType.ENTERPRISE, License.LicenseType.TRIAL);
 
     /**
      * Duration of grace period after a license has expired
@@ -79,7 +76,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
     static final TimeValue GRACE_PERIOD_DURATION = days(7);
 
     public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS =
-            XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
+        XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
 
     private final Settings settings;
 
@@ -117,7 +114,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
     private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
 
     private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
-            "please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
+        "please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
 
     public LicenseService(Settings settings, ClusterService clusterService, Clock clock, Environment env,
                           ResourceWatcherService resourceWatcherService, XPackLicenseState licenseState) {
@@ -199,7 +196,7 @@ 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("basic")) {
+        } else if (newLicense.type().equals(License.LicenseType.BASIC.getTypeName())) {
             listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed."));
         } else if (newLicense.expiryDate() < now) {
             listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED));
@@ -212,7 +209,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
                     if (acknowledgeMessages.isEmpty() == false) {
                         // needs acknowledgement
                         listener.onResponse(new PutLicenseResponse(false, LicensesStatus.VALID, ACKNOWLEDGEMENT_HEADER,
-                                acknowledgeMessages));
+                            acknowledgeMessages));
                         return;
                     }
                 }
@@ -239,36 +236,49 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
             }
 
             clusterService.submitStateUpdateTask("register license [" + newLicense.uid() + "]", new
-                    AckedClusterStateUpdateTask<PutLicenseResponse>(request, listener) {
-                        @Override
-                        protected PutLicenseResponse newResponse(boolean acknowledged) {
-                            return new PutLicenseResponse(acknowledged, LicensesStatus.VALID);
-                        }
+                AckedClusterStateUpdateTask<PutLicenseResponse>(request, listener) {
+                    @Override
+                    protected PutLicenseResponse newResponse(boolean acknowledged) {
+                        return new PutLicenseResponse(acknowledged, LicensesStatus.VALID);
+                    }
 
-                        @Override
-                        public ClusterState execute(ClusterState currentState) throws Exception {
-                            XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
-                            MetaData currentMetadata = currentState.metaData();
-                            LicensesMetaData licensesMetaData = currentMetadata.custom(LicensesMetaData.TYPE);
-                            Version trialVersion = null;
-                            if (licensesMetaData != null) {
-                                trialVersion = licensesMetaData.getMostRecentTrialVersion();
-                            }
-                            MetaData.Builder mdBuilder = MetaData.builder(currentMetadata);
-                            mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(newLicense, trialVersion));
-                            return ClusterState.builder(currentState).metaData(mdBuilder).build();
+                    @Override
+                    public ClusterState execute(ClusterState currentState) throws Exception {
+                        XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
+                        final Version oldestNodeVersion = currentState.nodes().getSmallestNonClientNodeVersion();
+                        if (licenseIsCompatible(newLicense, oldestNodeVersion) == false) {
+                            throw new IllegalStateException("The provided license is not compatible with node version [" +
+                                oldestNodeVersion + "]");
+                        }
+                        MetaData currentMetadata = currentState.metaData();
+                        LicensesMetaData licensesMetaData = currentMetadata.custom(LicensesMetaData.TYPE);
+                        Version trialVersion = null;
+                        if (licensesMetaData != null) {
+                            trialVersion = licensesMetaData.getMostRecentTrialVersion();
                         }
-                    });
+                        MetaData.Builder mdBuilder = MetaData.builder(currentMetadata);
+                        mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(newLicense, trialVersion));
+                        return ClusterState.builder(currentState).metaData(mdBuilder).build();
+                    }
+                });
+        }
+    }
+
+    private static boolean licenseIsCompatible(License license, Version version) {
+        if (License.LicenseType.ENTERPRISE.getTypeName().equalsIgnoreCase(license.type())) {
+            return version.onOrAfter(Version.V_8_0_0);
+        } else {
+            return true;
         }
     }
 
     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
-                && currentLicense.issueDate() > newLicense.issueDate()) { // and has a later issue date
-            acknowledgeMessages.put("license", new String[]{
-                    "The new license is older than the currently installed license. " +
-                            "Are you sure you want to override the current license?"});
+            && currentLicense.issueDate() > newLicense.issueDate()) { // and has a later issue date
+            acknowledgeMessages.put("license", new String[] {
+                "The new license is older than the currently installed license. " +
+                    "Are you sure you want to override the current license?" });
         }
         XPackLicenseState.ACKNOWLEDGMENT_MESSAGES.forEach((feature, ackMessages) -> {
             String[] messages = ackMessages.apply(currentLicense.operationMode(), newLicense.operationMode());
@@ -293,8 +303,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
                 updateLicenseState(license, licensesMetaData.getMostRecentTrialVersion());
             } else if (event.getJobName().startsWith(ExpirationCallback.EXPIRATION_JOB_PREFIX)) {
                 expirationCallbacks.stream()
-                        .filter(expirationCallback -> expirationCallback.getId().equals(event.getJobName()))
-                        .forEach(expirationCallback -> expirationCallback.on(license));
+                    .filter(expirationCallback -> expirationCallback.getId().equals(event.getJobName()))
+                    .forEach(expirationCallback -> expirationCallback.on(license));
             }
         }
     }
@@ -304,27 +314,27 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
      */
     public void removeLicense(final DeleteLicenseRequest request, final ActionListener<ClusterStateUpdateResponse> listener) {
         clusterService.submitStateUpdateTask("delete license",
-                new AckedClusterStateUpdateTask<ClusterStateUpdateResponse>(request, listener) {
-                    @Override
-                    protected ClusterStateUpdateResponse newResponse(boolean acknowledged) {
-                        return new ClusterStateUpdateResponse(acknowledged);
-                    }
+            new AckedClusterStateUpdateTask<ClusterStateUpdateResponse>(request, listener) {
+                @Override
+                protected ClusterStateUpdateResponse newResponse(boolean acknowledged) {
+                    return new ClusterStateUpdateResponse(acknowledged);
+                }
 
-                    @Override
-                    public ClusterState execute(ClusterState currentState) throws Exception {
-                        MetaData metaData = currentState.metaData();
-                        final LicensesMetaData currentLicenses = metaData.custom(LicensesMetaData.TYPE);
-                        if (currentLicenses.getLicense() != LicensesMetaData.LICENSE_TOMBSTONE) {
-                            MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
-                            LicensesMetaData newMetadata = new LicensesMetaData(LicensesMetaData.LICENSE_TOMBSTONE,
-                                    currentLicenses.getMostRecentTrialVersion());
-                            mdBuilder.putCustom(LicensesMetaData.TYPE, newMetadata);
-                            return ClusterState.builder(currentState).metaData(mdBuilder).build();
-                        } else {
-                            return currentState;
-                        }
+                @Override
+                public ClusterState execute(ClusterState currentState) throws Exception {
+                    MetaData metaData = currentState.metaData();
+                    final LicensesMetaData currentLicenses = metaData.custom(LicensesMetaData.TYPE);
+                    if (currentLicenses.getLicense() != LicensesMetaData.LICENSE_TOMBSTONE) {
+                        MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
+                        LicensesMetaData newMetadata = new LicensesMetaData(LicensesMetaData.LICENSE_TOMBSTONE,
+                            currentLicenses.getMostRecentTrialVersion());
+                        mdBuilder.putCustom(LicensesMetaData.TYPE, newMetadata);
+                        return ClusterState.builder(currentState).metaData(mdBuilder).build();
+                    } else {
+                        return currentState;
                     }
-                });
+                }
+            });
     }
 
     public License getLicense() {
@@ -337,9 +347,10 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
     }
 
     void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostStartTrialResponse> listener) {
-        if (VALID_TRIAL_TYPES.contains(request.getType()) == false) {
-            throw new IllegalArgumentException("Cannot start trial of type [" + request.getType() + "]. Valid trial types are "
-                    + VALID_TRIAL_TYPES + ".");
+        License.LicenseType requestedType = License.LicenseType.parse(request.getType());
+        if (VALID_TRIAL_TYPES.contains(requestedType) == false) {
+            throw new IllegalArgumentException("Cannot start trial of type [" + requestedType.getTypeName() + "]. Valid trial types are ["
+                + VALID_TRIAL_TYPES.stream().map(License.LicenseType::getTypeName).sorted().collect(Collectors.joining(",")) + "]");
         }
         StartTrialClusterTask task = new StartTrialClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener);
         clusterService.submitStateUpdateTask("started trial license", task);
@@ -358,7 +369,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
      */
     private void registerOrUpdateSelfGeneratedLicense() {
         clusterService.submitStateUpdateTask("maybe generate license for cluster",
-                new StartupSelfGeneratedLicenseTask(settings, clock, clusterService));
+            new StartupSelfGeneratedLicenseTask(settings, clock, clusterService));
     }
 
     @Override
@@ -369,11 +380,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
         if (clusterService.lifecycleState() == Lifecycle.State.STARTED) {
             final ClusterState clusterState = clusterService.state();
             if (clusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK) == false &&
-                    clusterState.nodes().getMasterNode() != null && XPackPlugin.isReadyForXPackCustomMetadata(clusterState)) {
+                clusterState.nodes().getMasterNode() != null && XPackPlugin.isReadyForXPackCustomMetadata(clusterState)) {
                 final LicensesMetaData currentMetaData = clusterState.metaData().custom(LicensesMetaData.TYPE);
                 boolean noLicense = currentMetaData == null || currentMetaData.getLicense() == null;
                 if (clusterState.getNodes().isLocalNodeElectedMaster() &&
-                        (noLicense || LicenseUtils.licenseNeedsExtended(currentMetaData.getLicense()))) {
+                    (noLicense || LicenseUtils.licenseNeedsExtended(currentMetaData.getLicense()))) {
                     // triggers a cluster changed event eventually notifying the current licensee
                     registerOrUpdateSelfGeneratedLicense();
                 }
@@ -416,7 +427,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
             }
             // notify all interested plugins
             if (previousClusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)
-                    || prevLicensesMetaData == null) {
+                || prevLicensesMetaData == null) {
                 if (currentLicensesMetaData != null) {
                     onUpdate(currentLicensesMetaData);
                 }
@@ -438,8 +449,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
             // auto-generate license if no licenses ever existed or if the current license is basic and
             // needs extended or if the license signature needs to be updated. this will trigger a subsequent cluster changed event
             if (currentClusterState.getNodes().isLocalNodeElectedMaster() &&
-                    (noLicense || LicenseUtils.licenseNeedsExtended(currentLicense) ||
-                        LicenseUtils.signatureNeedsUpdate(currentLicense, currentClusterState.nodes()))) {
+                (noLicense || LicenseUtils.licenseNeedsExtended(currentLicense) ||
+                    LicenseUtils.signatureNeedsUpdate(currentLicense, currentClusterState.nodes()))) {
                 registerOrUpdateSelfGeneratedLicense();
             }
         } else if (logger.isDebugEnabled()) {
@@ -501,15 +512,15 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
                 scheduler.add(new SchedulerEngine.Job(LICENSE_JOB, nextLicenseCheck(license)));
                 for (ExpirationCallback expirationCallback : expirationCallbacks) {
                     scheduler.add(new SchedulerEngine.Job(expirationCallback.getId(),
-                            (startTime, now) ->
-                                    expirationCallback.nextScheduledTimeForExpiry(license.expiryDate(), startTime, now)));
+                        (startTime, now) ->
+                            expirationCallback.nextScheduledTimeForExpiry(license.expiryDate(), startTime, now)));
                 }
                 if (previousLicense != null) {
                     // remove operationModeFileWatcher to gc the old license object
                     previousLicense.removeOperationModeFileWatcher();
                 }
                 logger.info("license [{}] mode [{}] - valid", license.uid(),
-                        license.operationMode().name().toLowerCase(Locale.ROOT));
+                    license.operationMode().name().toLowerCase(Locale.ROOT));
             }
             updateLicenseState(license, currentLicensesMetaData.getMostRecentTrialVersion());
         }
@@ -547,7 +558,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
             } else if (license != null) {
                 boolean autoGeneratedLicense = License.isAutoGeneratedLicense(license.signature());
                 if ((autoGeneratedLicense && SelfGeneratedLicense.verify(license))
-                        || (!autoGeneratedLicense && LicenseVerifier.verifyLicense(license))) {
+                    || (!autoGeneratedLicense && LicenseVerifier.verifyLicense(license))) {
                     return license;
                 }
             }

+ 5 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java

@@ -7,6 +7,7 @@ package org.elasticsearch.license;
 
 import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.license.License.LicenseType;
 import org.elasticsearch.rest.RestStatus;
 
 public class LicenseUtils {
@@ -36,7 +37,8 @@ public class LicenseUtils {
     }
 
     public static boolean licenseNeedsExtended(License license) {
-        return "basic".equals(license.type()) && license.expiryDate() != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
+        return LicenseType.isBasic(license.type()) &&
+            license.expiryDate() != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
     }
 
     /**
@@ -46,7 +48,8 @@ public class LicenseUtils {
     public static boolean signatureNeedsUpdate(License license, DiscoveryNodes currentNodes) {
         assert License.VERSION_CRYPTO_ALGORITHMS == License.VERSION_CURRENT : "update this method when adding a new version";
 
-        return ("basic".equals(license.type()) || "trial".equals(license.type())) &&
+        String typeName = license.type();
+        return (LicenseType.isBasic(typeName) || LicenseType.isTrial(typeName)) &&
                 // only upgrade signature when all nodes are ready to deserialize the new signature
                 (license.version() < License.VERSION_CRYPTO_ALGORITHMS &&
                     compatibleLicenseVersion(currentNodes) == License.VERSION_CRYPTO_ALGORITHMS

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesMetaData.java

@@ -39,7 +39,7 @@ public class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> imp
      * ever existed in the cluster state
      */
     public static final License LICENSE_TOMBSTONE = License.builder()
-            .type("trial")
+            .type(License.LicenseType.TRIAL)
             .issuer("elasticsearch")
             .uid("TOMBSTONE")
             .issuedTo("")

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java

@@ -29,7 +29,7 @@ public class RestPostStartTrialLicense extends BaseRestHandler {
     @Override
     protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
         PostStartTrialRequest startTrialRequest = new PostStartTrialRequest();
-        startTrialRequest.setType(request.param("type", "trial"));
+        startTrialRequest.setType(request.param("type", License.LicenseType.TRIAL.getTypeName()));
         startTrialRequest.acknowledge(request.paramAsBoolean("acknowledge", false));
         return channel -> client.execute(PostStartTrialAction.INSTANCE, startTrialRequest,
                 new RestBuilderListener<>(channel) {

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPutLicenseAction.java

@@ -41,9 +41,9 @@ public class RestPutLicenseAction extends BaseRestHandler {
         putLicenseRequest.timeout(request.paramAsTime("timeout", putLicenseRequest.timeout()));
         putLicenseRequest.masterNodeTimeout(request.paramAsTime("master_timeout", putLicenseRequest.masterNodeTimeout()));
 
-        if ("basic".equals(putLicenseRequest.license().type())) {
+        if (License.LicenseType.isBasic(putLicenseRequest.license().type())) {
             throw new IllegalArgumentException("Installing basic licenses is no longer allowed. Use the POST " +
-                    "/_license/start_basic API to install a basic license that does not expire.");
+                "/_license/start_basic API to install a basic license that does not expire.");
         }
 
         return channel -> client.execute(PutLicenseAction.INSTANCE, putLicenseRequest, new RestToXContentListener<>(channel));

+ 11 - 5
x-pack/plugin/core/src/main/java/org/elasticsearch/license/SelfGeneratedLicense.java

@@ -20,10 +20,10 @@ import java.nio.ByteBuffer;
 import java.util.Base64;
 import java.util.Collections;
 
-import static org.elasticsearch.license.CryptUtils.encryptV3Format;
-import static org.elasticsearch.license.CryptUtils.encrypt;
-import static org.elasticsearch.license.CryptUtils.decryptV3Format;
 import static org.elasticsearch.license.CryptUtils.decrypt;
+import static org.elasticsearch.license.CryptUtils.decryptV3Format;
+import static org.elasticsearch.license.CryptUtils.encrypt;
+import static org.elasticsearch.license.CryptUtils.encryptV3Format;
 
 class SelfGeneratedLicense {
 
@@ -83,7 +83,13 @@ class SelfGeneratedLicense {
         }
     }
 
-    public static boolean validSelfGeneratedType(String type) {
-        return "basic".equals(type) || "trial".equals(type);
+    static License.LicenseType validateSelfGeneratedType(License.LicenseType type) {
+        switch (type) {
+            case BASIC:
+            case TRIAL:
+                return type;
+        }
+        throw new IllegalArgumentException("invalid self generated license type [" + type + "], only " +
+            License.LicenseType.BASIC + " and " + License.LicenseType.TRIAL + " are accepted");
     }
 }

+ 3 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartBasicClusterTask.java

@@ -51,7 +51,7 @@ public class StartBasicClusterTask extends ClusterStateUpdateTask {
         if (acknowledgeMessages.isEmpty() == false) {
             listener.onResponse(new PostStartBasicResponse(PostStartBasicResponse.Status.NEED_ACKNOWLEDGEMENT, acknowledgeMessages,
                     ACKNOWLEDGEMENT_HEADER));
-        } else if (oldLicense != null && oldLicense.type().equals("basic")) {
+        } else if (oldLicense != null && License.LicenseType.isBasic(oldLicense.type())) {
             listener.onResponse(new PostStartBasicResponse(PostStartBasicResponse.Status.ALREADY_USING_BASIC));
         }  else {
             listener.onResponse(new PostStartBasicResponse(PostStartBasicResponse.Status.GENERATED_BASIC));
@@ -63,7 +63,7 @@ public class StartBasicClusterTask extends ClusterStateUpdateTask {
         XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
         LicensesMetaData licensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE);
         License currentLicense = LicensesMetaData.extractLicense(licensesMetaData);
-        if (currentLicense == null || currentLicense.type().equals("basic") == false) {
+        if (currentLicense == null || License.LicenseType.isBasic(currentLicense.type()) == false) {
             long issueDate = clock.millis();
             MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
             License.Builder specBuilder = License.builder()
@@ -71,7 +71,7 @@ public class StartBasicClusterTask extends ClusterStateUpdateTask {
                     .issuedTo(clusterName)
                     .maxNodes(LicenseService.SELF_GENERATED_LICENSE_MAX_NODES)
                     .issueDate(issueDate)
-                    .type("basic")
+                    .type(License.LicenseType.BASIC)
                     .expiryDate(LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS);
             License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentState.nodes());
             if (request.isAcknowledged() == false && currentLicense != null) {

+ 8 - 11
x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java

@@ -5,8 +5,8 @@
  */
 package org.elasticsearch.license;
 
-import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.message.ParameterizedMessage;
 import org.apache.logging.log4j.util.Supplier;
 import org.elasticsearch.Version;
@@ -54,11 +54,8 @@ public class StartupSelfGeneratedLicenseTask extends ClusterStateUpdateTask {
         final LicensesMetaData currentLicensesMetaData = metaData.custom(LicensesMetaData.TYPE);
         // do not generate a license if any license is present
         if (currentLicensesMetaData == null) {
-            String type = LicenseService.SELF_GENERATED_LICENSE_TYPE.get(settings);
-            if (SelfGeneratedLicense.validSelfGeneratedType(type) == false) {
-                throw new IllegalArgumentException("Illegal self generated license type [" + type +
-                        "]. Must be trial or basic.");
-            }
+            License.LicenseType type = SelfGeneratedLicense.validateSelfGeneratedType(
+                LicenseService.SELF_GENERATED_LICENSE_TYPE.get(settings));
             return updateWithLicense(currentState, type);
         } else if (LicenseUtils.signatureNeedsUpdate(currentLicensesMetaData.getLicense(), currentState.nodes())) {
             return updateLicenseSignature(currentState, currentLicensesMetaData);
@@ -76,7 +73,7 @@ public class StartupSelfGeneratedLicenseTask extends ClusterStateUpdateTask {
         long issueDate = license.issueDate();
         long expiryDate = license.expiryDate();
         // extend the basic license expiration date if needed since extendBasic will not be called now
-        if ("basic".equals(type) &&  expiryDate != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
+        if (License.LicenseType.isBasic(type) &&  expiryDate != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
             expiryDate = LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
         }
         License.Builder specBuilder = License.builder()
@@ -117,18 +114,18 @@ public class StartupSelfGeneratedLicenseTask extends ClusterStateUpdateTask {
                 .issuedTo(currentLicense.issuedTo())
                 .maxNodes(selfGeneratedLicenseMaxNodes)
                 .issueDate(currentLicense.issueDate())
-                .type("basic")
+                .type(License.LicenseType.BASIC)
                 .expiryDate(LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS);
         License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentLicense.version());
         Version trialVersion = currentLicenseMetadata.getMostRecentTrialVersion();
         return new LicensesMetaData(selfGeneratedLicense, trialVersion);
     }
 
-    private ClusterState updateWithLicense(ClusterState currentState, String type) {
+    private ClusterState updateWithLicense(ClusterState currentState, License.LicenseType type) {
         long issueDate = clock.millis();
         MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
         long expiryDate;
-        if ("basic".equals(type)) {
+        if (type == License.LicenseType.BASIC) {
             expiryDate = LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
         } else {
             expiryDate = issueDate + LicenseService.NON_BASIC_SELF_GENERATED_LICENSE_DURATION.getMillis();
@@ -142,7 +139,7 @@ public class StartupSelfGeneratedLicenseTask extends ClusterStateUpdateTask {
                 .expiryDate(expiryDate);
         License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentState.nodes());
         LicensesMetaData licensesMetaData;
-        if ("trial".equals(type)) {
+        if (License.LicenseType.TRIAL.equals(type)) {
             licensesMetaData = new LicensesMetaData(selfGeneratedLicense, Version.CURRENT);
         } else {
             licensesMetaData = new LicensesMetaData(selfGeneratedLicense, null);

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportGetBasicStatusAction.java

@@ -49,7 +49,7 @@ public class TransportGetBasicStatusAction extends TransportMasterNodeReadAction
             listener.onResponse(new GetBasicStatusResponse(true));
         } else {
             License license = licensesMetaData.getLicense();
-            listener.onResponse(new GetBasicStatusResponse(license == null || license.type().equals("basic") == false));
+            listener.onResponse(new GetBasicStatusResponse(license == null || License.LicenseType.isBasic(license.type()) == false));
         }
 
     }

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

@@ -47,10 +47,13 @@ public class LicenseOperationModeTests extends ESTestCase {
         assertResolve(OperationMode.PLATINUM, "PlAtINum", "platinum");
     }
 
+    public void testResolveEnterpriseAsPlatinum() {
+        assertResolve(OperationMode.PLATINUM, License.LicenseType.ENTERPRISE.getTypeName());
+        assertResolve(OperationMode.PLATINUM, License.LicenseType.ENTERPRISE.name());
+    }
+
     public void testResolveUnknown() {
-        // 'enterprise' is a type that exists in cloud but should be rejected under normal operation
-        // See https://github.com/elastic/x-plugins/issues/3371
-        String[] types = { "unknown", "fake", "enterprise" };
+        String[] types = { "unknown", "fake", "commercial" };
 
         for (String type : types) {
             try {
@@ -59,7 +62,7 @@ public class LicenseOperationModeTests extends ESTestCase {
                 fail(String.format(Locale.ROOT, "[%s] should not be recognized as an operation mode", type));
             }
             catch (IllegalArgumentException e) {
-                assertThat(e.getMessage(), equalTo("unknown type [" + type + "]"));
+                assertThat(e.getMessage(), equalTo("unknown license type [" + type + "]"));
             }
         }
     }

+ 44 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTests.java

@@ -6,21 +6,28 @@
 package org.elasticsearch.license;
 
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.test.ESTestCase;
 
 import java.nio.BufferUnderflowException;
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
 
+import static org.elasticsearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION;
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
 
 public class LicenseTests extends ESTestCase {
 
     public void testFromXContent() throws Exception {
-
         String licenseString = "{\"license\":" +
             "{\"uid\":\"4056779d-b823-4c12-a9cb-efa4a8d8c422\"," +
             "\"type\":\"gold\"," +
@@ -46,6 +53,39 @@ public class LicenseTests extends ESTestCase {
         assertThat(license.issueDate(), equalTo(1546589020459L));
     }
 
+    public void testLicenseToAndFromXContentForEveryLicenseType() throws Exception {
+        for (License.LicenseType type : License.LicenseType.values()) {
+            final License license1 = License.builder()
+                .uid(UUIDs.randomBase64UUID(random()))
+                .type(type)
+                .issueDate(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(randomIntBetween(1, 10)))
+                .expiryDate(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(randomIntBetween(1, 1000)))
+                .maxNodes(randomIntBetween(1, 100))
+                .issuedTo(randomAlphaOfLengthBetween(5, 50))
+                .issuer(randomAlphaOfLengthBetween(5, 50))
+                // We need a signature that parses correctly, but it doesn't need to verify
+                .signature("AAAAAgAAAA34V2kfTJVtvdL2LttwAAABmFJ6NGRnbEM3WVQrZVQwNkdKQmR1VytlMTMyM1J0dTZ1WGwyY2ZCVFhqMGtJU2gzZ3pnNTVpOW" +
+                    "F5Y1NaUkwyN2VsTEtCYnlZR2c5WWtjQ0phaDlhRjlDUXViUmUwMWhjSkE2TFcwSGdneTJHbUV4N2RHUWJxV20ybjRsZHRzV2xkN0ZmdDlYblJmNVc" +
+                    "xMlBWeU81V1hLUm1EK0V1dmF3cFdlSGZzTU5SZE1qUmFra3JkS1hCanBWVmVTaFFwV3BVZERzeG9Sci9rYnlJK2toODZXY09tNmFHUVNUL3IyUHEx" +
+                    "V3VSTlBneWNJcFQ0bXl0cmhNNnRwbE1CWE4zWjJ5eGFuWFo0NGhsb3B5WFd1eTdYbFFWQkxFVFFPSlBERlB0eVVJYXVSZ0lsR2JpRS9rN1h4MSsvN" +
+                    "UpOcGN6cU1NOHN1cHNtSTFIUGN1bWNGNEcxekhrblhNOXZ2VEQvYmRzQUFwbytUZEpRR3l6QU5oS2ZFSFdSbGxxNDZyZ0xvUHIwRjdBL2JqcnJnNG" +
+                    "FlK09Cek9pYlJ5Umc9PQAAAQAth77fQLF7CCEL7wA6Z0/UuRm/weECcsjW/50kBnPLO8yEs+9/bPa5LSU0bF6byEXOVeO0ebUQfztpjulbXh8TrBD" +
+                    "SG+6VdxGtohPo2IYPBaXzGs3LOOor6An/lhptxBWdwYmfbcp0m8mnXZh1vN9rmbTsZXnhBIoPTaRDwUBi3vJ3Ms3iLaEm4S8Slrfmtht2jUjgGZ2v" +
+                    "AeZ9OHU2YsGtrSpz6f")
+                .build();
+            XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION,
+                Strings.toString(license1));
+            License license2 = License.fromXContent(parser);
+            assertThat(license2, notNullValue());
+            assertThat(license2.type(), equalTo(type.getTypeName()));
+            assertThat(license2.uid(), equalTo(license1.uid()));
+            assertThat(license2.issuer(), equalTo(license1.issuer()));
+            assertThat(license2.issuedTo(), equalTo(license1.issuedTo()));
+            assertThat(license2.expiryDate(), equalTo(license1.expiryDate()));
+            assertThat(license2.issueDate(), equalTo(license1.issueDate()));
+        }
+    }
+
     public void testNotEnoughBytesFromXContent() throws Exception {
 
         String licenseString = "{\"license\": " +
@@ -86,6 +126,9 @@ public class LicenseTests extends ESTestCase {
                     License.fromSource(new BytesArray(licenseString.getBytes(StandardCharsets.UTF_8)),
                         XContentType.JSON);
                 });
+        // When parsing a license, we read the signature bytes to verify the _version_.
+        // Random alphabetic sig bytes will generate a bad version
+        assertThat(exception, throwableWithMessage(containsString("Unknown license version found")));
     }
 
     public void testUnableToBase64DecodeFromXContent() throws Exception {

+ 11 - 10
x-pack/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java

@@ -22,6 +22,7 @@ import java.util.Arrays;
 import java.util.Collection;
 
 import static org.elasticsearch.test.ESIntegTestCase.Scope.SUITE;
+import static org.hamcrest.Matchers.containsString;
 
 @ESIntegTestCase.ClusterScope(scope = SUITE)
 public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase {
@@ -67,21 +68,21 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
             assertEquals("basic", getLicenseResponse.license().type());
         });
 
-        String type = randomFrom(LicenseService.VALID_TRIAL_TYPES);
+        License.LicenseType type = randomFrom(LicenseService.VALID_TRIAL_TYPES);
 
         Request ackRequest = new Request("POST", "/_license/start_trial");
         ackRequest.addParameter("acknowledge", "true");
-        ackRequest.addParameter("type", type);
+        ackRequest.addParameter("type", type.getTypeName());
         Response response3 = restClient.performRequest(ackRequest);
         String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8));
         assertEquals(200, response3.getStatusLine().getStatusCode());
-        assertTrue(body3.contains("\"trial_was_started\":true"));
-        assertTrue(body3.contains("\"type\":\"" + type + "\""));
-        assertTrue(body3.contains("\"acknowledged\":true"));
+        assertThat(body3, containsString("\"trial_was_started\":true"));
+        assertThat(body3, containsString("\"type\":\"" + type.getTypeName() + "\""));
+        assertThat(body3, containsString("\"acknowledged\":true"));
 
         assertBusy(() -> {
             GetLicenseResponse postTrialLicenseResponse = licensingClient.prepareGetLicense().get();
-            assertEquals(type, postTrialLicenseResponse.license().type());
+            assertEquals(type.getTypeName(), postTrialLicenseResponse.license().type());
         });
 
         Response response4 = restClient.performRequest(new Request("GET", "/_license/trial_status"));
@@ -89,11 +90,11 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
         assertEquals(200, response4.getStatusLine().getStatusCode());
         assertEquals("{\"eligible_to_start_trial\":false}", body4);
 
-        String secondAttemptType = randomFrom(LicenseService.VALID_TRIAL_TYPES);
+        License.LicenseType secondAttemptType = randomFrom(LicenseService.VALID_TRIAL_TYPES);
 
         Request startTrialWhenStartedRequest = new Request("POST", "/_license/start_trial");
         startTrialWhenStartedRequest.addParameter("acknowledge", "true");
-        startTrialWhenStartedRequest.addParameter("type", secondAttemptType);
+        startTrialWhenStartedRequest.addParameter("type", secondAttemptType.getTypeName());
         ResponseException ex = expectThrows(ResponseException.class, () -> restClient.performRequest(startTrialWhenStartedRequest));
         Response response5 = ex.getResponse();
         String body5 = Streams.copyToString(new InputStreamReader(response5.getEntity().getContent(), StandardCharsets.UTF_8));
@@ -111,8 +112,8 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
         Response response = ex.getResponse();
         String body = Streams.copyToString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8));
         assertEquals(400, response.getStatusLine().getStatusCode());
-        assertTrue(body.contains("\"type\":\"illegal_argument_exception\""));
-        assertTrue(body.contains("\"reason\":\"Cannot start trial of type [basic]. Valid trial types are ["));
+        assertThat(body, containsString("\"type\":\"illegal_argument_exception\""));
+        assertThat(body, containsString("\"reason\":\"Cannot start trial of type [basic]. Valid trial types are ["));
     }
 
     private void ensureStartingWithBasic() throws Exception {