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:
parent
0d8aed8c2b
commit
592481e4ed
|
@ -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',
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue