Require acknowledgement to start_trial license (#30135)

This is related to #30134. It modifies the start_trial action to require
an acknowledgement parameter in the rest request to actually start the
trial license. There are backwards compatibility issues as prior ES
versions did not support this parameter. To handle this, it is assumed
that a request coming from a node prior to 6.3 is acknowledged. And
attempts to write a non-acknowledged request to a prior to 6.3 node will
throw an exception.

Additionally this PR adds messages about the trial license the user is
generating.
This commit is contained in:
Tim Brooks 2018-04-26 21:42:44 -04:00 committed by GitHub
parent 0d8aed8c2b
commit 592481e4ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 207 additions and 40 deletions

View File

@ -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',

View File

@ -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

View File

@ -41,11 +41,11 @@ public class LicensingClient {
client.execute(DeleteLicenseAction.INSTANCE, request, listener);
}
public PostStartTrialRequestBuilder preparePostUpgradeToTrial() {
public PostStartTrialRequestBuilder preparePostStartTrial() {
return new PostStartTrialRequestBuilder(client, PostStartTrialAction.INSTANCE);
}
public GetTrialStatusRequestBuilder prepareGetUpgradeToTrial() {
public GetTrialStatusRequestBuilder prepareGetStartTrial() {
return new GetTrialStatusRequestBuilder(client, GetTrialStatusAction.INSTANCE);
}

View File

@ -15,6 +15,7 @@ import java.io.IOException;
public class PostStartTrialRequest extends MasterNodeRequest<PostStartTrialRequest> {
private boolean acknowledge = false;
private String type;
@Override
@ -31,25 +32,47 @@ public class PostStartTrialRequest extends MasterNodeRequest<PostStartTrialReque
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 {
if ("trial".equals(type) == false) {
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() + "].");
+ "] 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);
}
}
}
}

View File

@ -14,4 +14,9 @@ class PostStartTrialRequestBuilder extends ActionRequestBuilder<PostStartTrialRe
PostStartTrialRequestBuilder(ElasticsearchClient client, PostStartTrialAction action) {
super(client, action, new PostStartTrialRequest());
}
public PostStartTrialRequestBuilder setAcknowledge(boolean acknowledge) {
request.acknowledge(acknowledge);
return this;
}
}

View File

@ -5,23 +5,33 @@
*/
package org.elasticsearch.license;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.rest.RestStatus;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
class PostStartTrialResponse extends ActionResponse {
// Nodes Prior to 6.3 did not have NEED_ACKNOWLEDGEMENT as part of status
enum Pre63Status {
UPGRADED_TO_TRIAL,
TRIAL_ALREADY_ACTIVATED;
}
enum Status {
UPGRADED_TO_TRIAL(true, null, RestStatus.OK),
TRIAL_ALREADY_ACTIVATED(false, "Operation failed: Trial was already activated.", RestStatus.FORBIDDEN);
TRIAL_ALREADY_ACTIVATED(false, "Operation failed: Trial was already activated.", RestStatus.FORBIDDEN),
NEED_ACKNOWLEDGEMENT(false,"Operation failed: Needs acknowledgement.", RestStatus.OK);
private final boolean isTrialStarted;
private final String errorMessage;
private final RestStatus restStatus;
Status(boolean isTrialStarted, String errorMessage, RestStatus restStatus) {
this.isTrialStarted = isTrialStarted;
this.errorMessage = errorMessage;
@ -39,15 +49,24 @@ class PostStartTrialResponse extends ActionResponse {
RestStatus getRestStatus() {
return restStatus;
}
}
private Status status;
private Map<String, String[]> acknowledgeMessages;
private String acknowledgeMessage;
PostStartTrialResponse() {
}
PostStartTrialResponse(Status status) {
this(status, Collections.emptyMap(), null);
}
PostStartTrialResponse(Status status, Map<String, String[]> acknowledgeMessages, String acknowledgeMessage) {
this.status = status;
this.acknowledgeMessages = acknowledgeMessages;
this.acknowledgeMessage = acknowledgeMessage;
}
public Status getStatus() {
@ -57,10 +76,58 @@ class PostStartTrialResponse extends ActionResponse {
@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<String, String[]> 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 {
// 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<String, String[]> 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<String, String[]> getAcknowledgementMessages() {
return acknowledgeMessages;
}
String getAcknowledgementMessage() {
return acknowledgeMessage;
}
}

View File

@ -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<GetTrialStatusResponse>(channel) {
@Override
public RestResponse buildResponse(GetTrialStatusResponse response, XContentBuilder builder) throws Exception {

View File

@ -16,6 +16,7 @@ import org.elasticsearch.xpack.core.XPackClient;
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<PostStartTrialResponse>(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<String, String[]> acknowledgementMessages = response.getAcknowledgementMessages();
if (acknowledgementMessages.isEmpty() == false) {
builder.startObject("acknowledge");
builder.field("message", response.getAcknowledgementMessage());
for (Map.Entry<String, String[]> 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);
}
});

View File

@ -15,10 +15,23 @@ import org.elasticsearch.cluster.metadata.MetaData;
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<String, String[]> 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 class StartTrialClusterTask extends ClusterStateUpdateTask {
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 class StartTrialClusterTask extends ClusterStateUpdateTask {
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();

View File

@ -56,33 +56,47 @@ public class StartTrialLicenseTests extends AbstractLicensesIntegrationTestCase
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 getLicenseResponse = licensingClient.prepareGetLicense().get();
assertEquals("basic", getLicenseResponse.license().type());
});
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());
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 response3 = restClient.performRequest("GET", "/_xpack/license/trial_status");
String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response3.getStatusLine().getStatusCode());
assertEquals("{\"eligible_to_start_trial\":false}", body3);
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 {

View File

@ -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)"
}
}
},

View File

@ -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: