Skip to content

Commit

Permalink
Adding a warning header when a license is about to expire
Browse files Browse the repository at this point in the history
Resolves elastic#60562
  • Loading branch information
BigPandaToo committed Nov 19, 2020
1 parent ffcd72b commit 34efa0e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
*/
static final TimeValue GRACE_PERIOD_DURATION = days(7);

/**
* Period before the license expires when warning starts being added to the response header
*/
static final TimeValue LICENSE_EXPIRATION_WARNING_PERIOD = days(7);

public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS =
XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.elasticsearch.Version;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.logging.HeaderWarning;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -20,6 +21,7 @@
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand All @@ -32,7 +34,7 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.elasticsearch.license.LicenseService.GRACE_PERIOD_DURATION;
import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;

/**
* A holder for the current state of the license for all xpack features.
Expand Down Expand Up @@ -514,6 +516,7 @@ public boolean isActive() {
/**
* Checks whether the given feature is allowed, tracking the last usage time.
*/
@SuppressForbidden(reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE")
public boolean checkFeature(Feature feature) {
boolean allowed = isAllowed(feature);
LongAccumulator maxEpochAccumulator = lastUsed.get(feature);
Expand All @@ -523,9 +526,12 @@ public boolean checkFeature(Feature feature) {
}

if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 && isLicenseExpiring(now)) {
HeaderWarning.addWarning("Your license will expire in [{}] days. " +
"Contact your administrator or update your license for continued use of features",
TimeUnit.MILLISECONDS.toDays(status.licenseExpiryDate - now));
final long days = TimeUnit.MILLISECONDS.toDays(status.licenseExpiryDate - now);
final String expiryMessage = days == 0? "expires today":
(days > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
String.format(Locale.ROOT, "has expired [%d] days ago", Math.abs(days)));
HeaderWarning.addWarning("Your license {}. " +
"Contact your administrator or update your license for continued use of features", expiryMessage);
}

return allowed;
Expand Down Expand Up @@ -645,15 +651,15 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
}

/**
* Test whether current license expires in less than {@code GRACE_PERIOD_DURATION}.
* Test whether current license expires in less than {@code LICENSE_EXPIRATION_WARNING_PERIOD}.
*
* @param now Current time in milliseconds
*
* @return true if current license expires in less than {@code GRACE_PERIOD_DURATION}, otherwise false
* @return true if current license expires in less than {@code LICENSE_EXPIRATION_WARNING_PERIOD}, otherwise false
*/
public boolean isLicenseExpiring(long now) {
return checkAgainstStatus(status -> {
if (now > status.licenseExpiryDate - GRACE_PERIOD_DURATION.getMillis()) {
if (now > status.licenseExpiryDate - LICENSE_EXPIRATION_WARNING_PERIOD.getMillis()) {
return true;
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.elasticsearch.license;

import org.apache.http.Header;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
Expand All @@ -15,6 +16,10 @@
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.discovery.DiscoveryModule;
import org.elasticsearch.license.License.OperationMode;
Expand All @@ -25,6 +30,7 @@
import org.elasticsearch.test.MockHttpTransport;
import org.elasticsearch.test.SecurityIntegTestCase;
import org.elasticsearch.test.SecuritySettingsSource;
import org.elasticsearch.test.SecuritySettingsSourceField;
import org.elasticsearch.transport.Netty4Plugin;
import org.elasticsearch.transport.TransportInfo;
import org.elasticsearch.xpack.core.XPackField;
Expand All @@ -43,7 +49,9 @@

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -192,6 +200,56 @@ public void testNodeJoinWithoutSecurityExplicitlyEnabled() throws Exception {
}
}

public void testWarningHeader() throws Exception {
Request request = new Request("GET", "/_security/user");
RequestOptions.Builder options = request.getOptions().toBuilder();
options.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME,
new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray())));
request.setOptions(options);

Response response = getRestClient().performRequest(request);

List<String> beforeWarningHeaders = getWarningHeaders(response.getHeaders());

assertTrue(beforeWarningHeaders.isEmpty());

License.OperationMode mode = randomFrom(License.OperationMode.GOLD, License.OperationMode.PLATINUM,
License.OperationMode.ENTERPRISE, License.OperationMode.STANDARD);
long now = System.currentTimeMillis();
long newExpirationDate = now + LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() - 1;
setLicensingExpirationDate(mode, newExpirationDate);

response = getRestClient().performRequest(request);

List<String> afterWarningHeaders= getWarningHeaders(response.getHeaders());

assertTrue(afterWarningHeaders.size() == 1);
assertTrue(afterWarningHeaders.stream().anyMatch(v ->v.contains("Your license will expire in [6] days. " +
"Contact your administrator or update your license for continued use of features")));

newExpirationDate = now;
setLicensingExpirationDate(mode, newExpirationDate);

response = getRestClient().performRequest(request);

afterWarningHeaders= getWarningHeaders(response.getHeaders());

assertTrue(afterWarningHeaders.size() == 1);
assertTrue(afterWarningHeaders.stream().anyMatch(v ->v.contains("Your license expires today. " +
"Contact your administrator or update your license for continued use of features")));

newExpirationDate = now - TimeUnit.DAYS.toMillis(2);
setLicensingExpirationDate(mode, newExpirationDate);

response = getRestClient().performRequest(request);

afterWarningHeaders= getWarningHeaders(response.getHeaders());

assertTrue(afterWarningHeaders.size() == 1);
assertTrue(afterWarningHeaders.stream().anyMatch(v ->v.contains("Your license has expired [2] days ago. " +
"Contact your administrator or update your license for continued use of features")));
}

private static void assertElasticsearchSecurityException(ThrowingRunnable runnable) {
ElasticsearchSecurityException ee = expectThrows(ElasticsearchSecurityException.class, runnable);
assertThat(ee.getMetadata(LicenseUtils.EXPIRED_FEATURE_METADATA), hasItem(XPackField.SECURITY));
Expand Down Expand Up @@ -239,4 +297,38 @@ private void enableLicensing(License.OperationMode operationMode) throws Excepti
}
}, 30L, TimeUnit.SECONDS);
}

private void setLicensingExpirationDate(License.OperationMode operationMode, long expirationDate) throws Exception {
// do this in an await busy since there is a chance that the setting expiration date of the license is
// overwritten by some other cluster activity and the node throws an exception while we
// wait for things to stabilize!
assertBusy(() -> {
// first update the license so we can execute monitoring actions
for (XPackLicenseState licenseState : internalCluster().getInstances(XPackLicenseState.class)) {
licenseState.update(operationMode, true, expirationDate, null);
}

ensureGreen();
ensureClusterSizeConsistency();
ensureClusterStateConsistency();

// re-apply the update in case any node received an updated cluster state that triggered the license state
// to change
for (XPackLicenseState licenseState : internalCluster().getInstances(XPackLicenseState.class)) {
licenseState.update(operationMode, true, expirationDate, null);
}
}, 30L, TimeUnit.SECONDS);
}

private List<String> getWarningHeaders(Header[] headers) {
List<String> warnings = new ArrayList<>();

for (Header header : headers) {
if (header.getName().equals("Warning")) {
warnings.add(header.getValue());
}
}

return warnings;
}
}

0 comments on commit 34efa0e

Please sign in to comment.