diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index ae851732daac8..ab9bc99459968 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -61,7 +61,6 @@ buildRestTests.expectedUnconvertedCandidates = [ 'en/watcher/trigger/schedule/yearly.asciidoc', 'en/watcher/troubleshooting.asciidoc', 'en/rest-api/license/delete-license.asciidoc', - 'en/rest-api/license/start-trial.asciidoc', 'en/rest-api/license/update-license.asciidoc', 'en/ml/api-quickref.asciidoc', 'en/rest-api/ml/delete-calendar-event.asciidoc', diff --git a/x-pack/docs/en/rest-api/license/start-trial.asciidoc b/x-pack/docs/en/rest-api/license/start-trial.asciidoc index 8ff793455a239..7754f6feef79c 100644 --- a/x-pack/docs/en/rest-api/license/start-trial.asciidoc +++ b/x-pack/docs/en/rest-api/license/start-trial.asciidoc @@ -40,7 +40,7 @@ The following example checks whether you are eligible to start a trial: [source,js] ------------------------------------------------------------ -POST _xpack/license/start_trial +GET _xpack/license/start_trial ------------------------------------------------------------ // CONSOLE // TEST[skip:license testing issues] @@ -49,6 +49,27 @@ Example response: [source,js] ------------------------------------------------------------ { - "trial_was_started": true + "eligible_to_start_trial": true } ------------------------------------------------------------ +// NOTCONSOLE + +The following example starts a 30-day trial license. The acknowledge +parameter is required as you are initiating a license that will expire. + +[source,js] +------------------------------------------------------------ +POST _xpack/license/start_trial?acknowledge=true +------------------------------------------------------------ +// CONSOLE +// TEST[skip:license testing issues] + +Example response: +[source,js] +------------------------------------------------------------ +{ + "trial_was_started": true, + "acknowledged": true +} +------------------------------------------------------------ +// NOTCONSOLE \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java index d2d4461b93108..21381b376925d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java @@ -41,11 +41,11 @@ public void deleteLicense(DeleteLicenseRequest request, ActionListener { + private boolean acknowledge = false; private String type; @Override @@ -31,25 +32,47 @@ public String getType() { return type; } + public PostStartTrialRequest acknowledge(boolean acknowledge) { + this.acknowledge = acknowledge; + return this; + } + + public boolean isAcknowledged() { + return acknowledge; + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); if (in.getVersion().onOrAfter(Version.V_6_3_0)) { type = in.readString(); + acknowledge = in.readBoolean(); } else { type = "trial"; + acknowledge = true; } } @Override public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - Version version = Version.V_6_3_0; + // TODO: Change to 6.3 after backport + Version version = Version.V_7_0_0_alpha1; if (out.getVersion().onOrAfter(version)) { + super.writeTo(out); out.writeString(type); + out.writeBoolean(acknowledge); } else { - throw new IllegalArgumentException("All nodes in cluster must be version [" + version - + "] or newer to use `type` parameter. Attempting to write to node with version [" + out.getVersion() + "]."); + if ("trial".equals(type) == false) { + throw new IllegalArgumentException("All nodes in cluster must be version [" + version + + "] or newer to start trial with a different type than 'trial'. Attempting to write to " + + "a node with version [" + out.getVersion() + "] with trial type [" + type + "]."); + } else if (acknowledge == false) { + throw new IllegalArgumentException("Request must be acknowledged to send to a node with a version " + + "prior to [" + version + "]. Attempting to send request to node with version [" + out.getVersion() + "] " + + "without acknowledgement."); + } else { + super.writeTo(out); + } } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequestBuilder.java index af381e13517f8..6b0beba171bdd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PostStartTrialRequestBuilder.java @@ -14,4 +14,9 @@ class PostStartTrialRequestBuilder extends ActionRequestBuilder acknowledgeMessages; + private String acknowledgeMessage; PostStartTrialResponse() { } PostStartTrialResponse(Status status) { + this(status, Collections.emptyMap(), null); + } + + PostStartTrialResponse(Status status, Map acknowledgeMessages, String acknowledgeMessage) { this.status = status; + this.acknowledgeMessages = acknowledgeMessages; + this.acknowledgeMessage = acknowledgeMessage; } public Status getStatus() { @@ -57,10 +76,58 @@ public Status getStatus() { @Override public void readFrom(StreamInput in) throws IOException { status = in.readEnum(Status.class); + // TODO: Change to 6.3 after backport + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + acknowledgeMessage = in.readOptionalString(); + int size = in.readVInt(); + Map acknowledgeMessages = new HashMap<>(size); + for (int i = 0; i < size; i++) { + String feature = in.readString(); + int nMessages = in.readVInt(); + String[] messages = new String[nMessages]; + for (int j = 0; j < nMessages; j++) { + messages[j] = in.readString(); + } + acknowledgeMessages.put(feature, messages); + } + this.acknowledgeMessages = acknowledgeMessages; + } else { + this.acknowledgeMessages = Collections.emptyMap(); + } } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeEnum(status); + // TODO: Change to 6.3 after backport + Version version = Version.V_7_0_0_alpha1; + if (out.getVersion().onOrAfter(version)) { + out.writeEnum(status); + out.writeOptionalString(acknowledgeMessage); + out.writeVInt(acknowledgeMessages.size()); + for (Map.Entry entry : acknowledgeMessages.entrySet()) { + out.writeString(entry.getKey()); + out.writeVInt(entry.getValue().length); + for (String message : entry.getValue()) { + out.writeString(message); + } + } + } else { + if (status == Status.UPGRADED_TO_TRIAL) { + out.writeEnum(Pre63Status.UPGRADED_TO_TRIAL); + } else if (status == Status.TRIAL_ALREADY_ACTIVATED) { + out.writeEnum(Pre63Status.TRIAL_ALREADY_ACTIVATED); + } else { + throw new IllegalArgumentException("Starting trial on node with version [" + Version.CURRENT + "] requires " + + "acknowledgement parameter."); + } + } + } + + Map getAcknowledgementMessages() { + return acknowledgeMessages; + } + + String getAcknowledgementMessage() { + return acknowledgeMessage; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetTrialStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetTrialStatus.java index ebd43318ff91e..a136f2a88a65d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetTrialStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetTrialStatus.java @@ -29,7 +29,7 @@ public class RestGetTrialStatus extends XPackRestHandler { @Override protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException { - return channel -> client.licensing().prepareGetUpgradeToTrial().execute( + return channel -> client.licensing().prepareGetStartTrial().execute( new RestBuilderListener(channel) { @Override public RestResponse buildResponse(GetTrialStatusResponse response, XContentBuilder builder) throws Exception { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java index 0332eedd69dd1..af738b9aadf7f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestPostStartTrialLicense.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.core.rest.XPackRestHandler; import java.io.IOException; +import java.util.Map; import static org.elasticsearch.rest.RestRequest.Method.POST; @@ -30,23 +31,36 @@ public class RestPostStartTrialLicense extends XPackRestHandler { protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException { PostStartTrialRequest startTrialRequest = new PostStartTrialRequest(); startTrialRequest.setType(request.param("type", "trial")); + startTrialRequest.acknowledge(request.paramAsBoolean("acknowledge", false)); return channel -> client.licensing().postStartTrial(startTrialRequest, new RestBuilderListener(channel) { @Override public RestResponse buildResponse(PostStartTrialResponse response, XContentBuilder builder) throws Exception { PostStartTrialResponse.Status status = response.getStatus(); + builder.startObject(); + builder.field("acknowledged", startTrialRequest.isAcknowledged()); if (status.isTrialStarted()) { - builder.startObject() - .field("trial_was_started", true) - .field("type", startTrialRequest.getType()) - .endObject(); + builder.field("trial_was_started", true); + builder.field("type", startTrialRequest.getType()); } else { - builder.startObject() - .field("trial_was_started", false) - .field("error_message", status.getErrorMessage()) - .endObject(); + builder.field("trial_was_started", false); + builder.field("error_message", status.getErrorMessage()); + } + Map acknowledgementMessages = response.getAcknowledgementMessages(); + if (acknowledgementMessages.isEmpty() == false) { + builder.startObject("acknowledge"); + builder.field("message", response.getAcknowledgementMessage()); + for (Map.Entry entry : acknowledgementMessages.entrySet()) { + builder.startArray(entry.getKey()); + for (String message : entry.getValue()) { + builder.value(message); + } + builder.endArray(); + } + builder.endObject(); } + builder.endObject(); return new BytesRestResponse(status.getRestStatus(), builder); } }); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java index 3ca8dbf0eaa4e..355672dedf717 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java @@ -15,10 +15,23 @@ import org.elasticsearch.common.Nullable; import java.time.Clock; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; public class StartTrialClusterTask extends ClusterStateUpdateTask { + private static final String ACKNOWLEDGEMENT_HEADER = "This API initiates a free 30-day trial for all platinum features. " + + "By starting this trial, you agree that it is subject to the terms and conditions at" + + " https://www.elastic.co/legal/trial_license/. To begin your free trial, call /start_trial again and specify " + + "the \"acknowledge=true\" parameter."; + + private static final Map ACK_MESSAGES = Collections.singletonMap("security", + new String[] {"With a trial license, X-Pack security features are available, but are not enabled by default."}); + private final Logger logger; private final String clusterName; private final PostStartTrialRequest request; @@ -39,7 +52,10 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS LicensesMetaData oldLicensesMetaData = oldState.metaData().custom(LicensesMetaData.TYPE); logger.debug("started self generated trial license: {}", oldLicensesMetaData); - if (oldLicensesMetaData == null || oldLicensesMetaData.isEligibleForTrial()) { + if (request.isAcknowledged() == false) { + listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.NEED_ACKNOWLEDGEMENT, + ACK_MESSAGES, ACKNOWLEDGEMENT_HEADER)); + } else if (oldLicensesMetaData == null || oldLicensesMetaData.isEligibleForTrial()) { listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.UPGRADED_TO_TRIAL)); } else { listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.Status.TRIAL_ALREADY_ACTIVATED)); @@ -50,7 +66,9 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS public ClusterState execute(ClusterState currentState) throws Exception { LicensesMetaData currentLicensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE); - if (currentLicensesMetaData == null || currentLicensesMetaData.isEligibleForTrial()) { + if (request.isAcknowledged() == false) { + return currentState; + } else if (currentLicensesMetaData == null || currentLicensesMetaData.isEligibleForTrial()) { long issueDate = clock.millis(); MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); long expiryDate = issueDate + LicenseService.NON_BASIC_SELF_GENERATED_LICENSE_DURATION.getMillis(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java index d673c4e720452..b7a09d24b1359 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/StartTrialLicenseTests.java @@ -56,33 +56,47 @@ public void testStartTrial() throws Exception { assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals("{\"eligible_to_start_trial\":true}", body); - String type = randomFrom(LicenseService.VALID_TRIAL_TYPES); - - Response response2 = restClient.performRequest("POST", "/_xpack/license/start_trial?type=" + type); + // Test that starting will fail without acknowledgement + Response response2 = restClient.performRequest("POST", "/_xpack/license/start_trial"); String body2 = Streams.copyToString(new InputStreamReader(response2.getEntity().getContent(), StandardCharsets.UTF_8)); assertEquals(200, response2.getStatusLine().getStatusCode()); - assertTrue(body2.contains("\"trial_was_started\":true")); - assertTrue(body2.contains("\"type\":\"" + type + "\"")); + assertTrue(body2.contains("\"trial_was_started\":false")); + assertTrue(body2.contains("\"error_message\":\"Operation failed: Needs acknowledgement.\"")); + assertTrue(body2.contains("\"acknowledged\":false")); assertBusy(() -> { - GetLicenseResponse postTrialLicenseResponse = licensingClient.prepareGetLicense().get(); - assertEquals(type, postTrialLicenseResponse.license().type()); + GetLicenseResponse getLicenseResponse = licensingClient.prepareGetLicense().get(); + assertEquals("basic", getLicenseResponse.license().type()); }); - Response response3 = restClient.performRequest("GET", "/_xpack/license/trial_status"); + String type = randomFrom(LicenseService.VALID_TRIAL_TYPES); + + Response response3 = restClient.performRequest("POST", "/_xpack/license/start_trial?acknowledge=true&type=" + type); String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8)); assertEquals(200, response3.getStatusLine().getStatusCode()); - assertEquals("{\"eligible_to_start_trial\":false}", body3); + assertTrue(body3.contains("\"trial_was_started\":true")); + assertTrue(body3.contains("\"type\":\"" + type + "\"")); + assertTrue(body3.contains("\"acknowledged\":true")); + + assertBusy(() -> { + GetLicenseResponse postTrialLicenseResponse = licensingClient.prepareGetLicense().get(); + assertEquals(type, postTrialLicenseResponse.license().type()); + }); + + Response response4 = restClient.performRequest("GET", "/_xpack/license/trial_status"); + String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8)); + assertEquals(200, response4.getStatusLine().getStatusCode()); + assertEquals("{\"eligible_to_start_trial\":false}", body4); String secondAttemptType = randomFrom(LicenseService.VALID_TRIAL_TYPES); ResponseException ex = expectThrows(ResponseException.class, - () -> restClient.performRequest("POST", "/_xpack/license/start_trial?type=" + secondAttemptType)); - Response response4 = ex.getResponse(); - String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8)); - assertEquals(403, response4.getStatusLine().getStatusCode()); - assertTrue(body4.contains("\"trial_was_started\":false")); - assertTrue(body4.contains("\"error_message\":\"Operation failed: Trial was already activated.\"")); + () -> restClient.performRequest("POST", "/_xpack/license/start_trial?acknowledge=true&type=" + secondAttemptType)); + Response response5 = ex.getResponse(); + String body5 = Streams.copyToString(new InputStreamReader(response5.getEntity().getContent(), StandardCharsets.UTF_8)); + assertEquals(403, response5.getStatusLine().getStatusCode()); + assertTrue(body5.contains("\"trial_was_started\":false")); + assertTrue(body5.contains("\"error_message\":\"Operation failed: Trial was already activated.\"")); } public void testInvalidType() throws Exception { diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json index 688afc7b79bbf..a1e5d27da1eda 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.license.post_start_trial.json @@ -11,6 +11,10 @@ "type": { "type" : "string", "description" : "The type of trial license to generate (default: \"trial\")" + }, + "acknowledge": { + "type" : "boolean", + "description" : "whether the user has acknowledged acknowledge messages (default: false)" } } }, diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml index 98e96318d7a19..9eb3b79fda7a7 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml @@ -133,7 +133,8 @@ teardown: - do: catch: forbidden - xpack.license.post_start_trial: {} + xpack.license.post_start_trial: + acknowledge: true - match: { trial_was_started: false } - match: { error_message: "Operation failed: Trial was already activated." } @@ -143,6 +144,7 @@ teardown: catch: bad_request xpack.license.post_start_trial: type: "basic" + acknowledge: true --- "Can start basic license if do not already have basic": - do: