Skip to content

Commit

Permalink
Adding a warning header when a license is about to expire #64948 (#65900
Browse files Browse the repository at this point in the history
)

* Adding a warning header when a license is about to expire (#64948)

* This change adds a warning header when a license is about to expire

Resolves #60562

* This change adds realm name of the realm used to perform authentication to the responses of _security/oidc/authenticate and _security/oidc/authenticate APIs

Resolves #53161

* Adding doc for the new API introduced by #64517 - /_security/saml/metadata/{realm}

Related to #49018

* Adding a warning header when a license is about to expire

Resolves #60562

* Addressing the PR feedback

* Switching back to adding the header during featureCheck to allow
warnings when authentication is disabled as well. Adding filterHeader
implementation to SecurityRestFilter exception handling to remove all
the warnings if authentication fails.

* Changing the wording for "expired" message to be consistent with the log
 messages; changing "today" calculation; adding a test case for failing
 authN to make sure we remove the warning header

* Small changes in the way we verify header in tests

* Nit changes

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

* Resolving backporting issue: adding copyMapWithRemovedEntry() util function
Fixing unused imports

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
BigPandaToo and elasticmachine committed Dec 5, 2020
1 parent 050c7eb commit e0b75c9
Show file tree
Hide file tree
Showing 19 changed files with 246 additions and 56 deletions.
35 changes: 35 additions & 0 deletions server/src/main/java/org/elasticsearch/common/util/Maps.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@

package org.elasticsearch.common.util;

import org.elasticsearch.Assertions;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class Maps {

Expand All @@ -43,4 +47,35 @@ public static <K, V> boolean deepEquals(Map<K, V> left, Map<K, V> right) {
.allMatch(e -> right.containsKey(e.getKey()) && Objects.deepEquals(e.getValue(), right.get(e.getKey())));
}

/**
* Remove the specified key from the provided immutable map by copying the underlying map and filtering out the specified
* key if that key exists.
*
* @param map the immutable map to remove the key from
* @param key the key to be removed
* @param <K> the type of the keys in the map
* @param <V> the type of the values in the map
* @return an immutable map that contains the items from the specified map with the provided key removed
*/
public static <K, V> Map<K, V> copyMapWithRemovedEntry(final Map<K, V> map, final K key) {
Objects.requireNonNull(map);
Objects.requireNonNull(key);
assertImmutableMap(map, key, map.get(key));
return map.entrySet().stream().filter(k -> key.equals(k.getKey()) == false)
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue),
Collections::<K, V>unmodifiableMap));
}

private static <K, V> void assertImmutableMap(final Map<K, V> map, final K key, final V value) {
if (Assertions.ENABLED) {
boolean immutable;
try {
map.put(key, value);
immutable = false;
} catch (final UnsupportedOperationException e) {
immutable = true;
}
assert immutable : "expected an immutable map but was [" + map.getClass() + "]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void sendResponse(RestResponse restResponse) {

// Add all custom headers
addCustomHeaders(httpResponse, restResponse.getHeaders());
addCustomHeaders(httpResponse, threadContext.getResponseHeaders());
addCustomHeaders(httpResponse, restResponse.filterHeaders(threadContext.getResponseHeaders()));

// If our response doesn't specify a content-type header, set one
setHeaderField(httpResponse, CONTENT_TYPE, restResponse.contentType(), false);
Expand Down
4 changes: 4 additions & 0 deletions server/src/main/java/org/elasticsearch/rest/RestResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,8 @@ public Map<String, List<String>> getHeaders() {
return customHeaders;
}
}

public Map<String, List<String>> filterHeaders(Map<String, List<String>> headers) {
return headers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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 Expand Up @@ -125,7 +130,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste

public static final String LICENSE_JOB = "licenseJob";

private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
public 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:";
Expand Down Expand Up @@ -476,7 +481,7 @@ private void updateLicenseState(LicensesMetadata licensesMetadata) {
protected void updateLicenseState(final License license, Version mostRecentTrialVersion) {
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
// implies license has been explicitly deleted
licenseState.update(License.OperationMode.MISSING, false, mostRecentTrialVersion);
licenseState.update(License.OperationMode.MISSING, false, license.expiryDate(), mostRecentTrialVersion);
return;
}
if (license != null) {
Expand All @@ -489,7 +494,7 @@ protected void updateLicenseState(final License license, Version mostRecentTrial
// date that is near Long.MAX_VALUE
active = time >= license.issueDate() && time - GRACE_PERIOD_DURATION.getMillis() < license.expiryDate();
}
licenseState.update(license.operationMode(), active, mostRecentTrialVersion);
licenseState.update(license.operationMode(), active, license.expiryDate(), mostRecentTrialVersion);

if (active) {
if (time < license.expiryDate()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public interface LicenseStateListener {

/**
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, Version)}.
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, long, Version)}.
*/
void licenseStateChanged();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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;
import org.elasticsearch.license.License.OperationMode;
Expand All @@ -19,17 +21,21 @@
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;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;

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 @@ -399,7 +405,7 @@ private static boolean isBasic(OperationMode mode) {
return mode == OperationMode.BASIC;
}

/** A wrapper for the license mode and state, to allow atomically swapping. */
/** A wrapper for the license mode, state, and expiration date, to allow atomically swapping. */
private static class Status {

/** The current "mode" of the license (ie license type). */
Expand All @@ -408,9 +414,13 @@ private static class Status {
/** True if the license is active, or false if it is expired. */
final boolean active;

Status(OperationMode mode, boolean active) {
/** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
final long licenseExpiryDate;

Status(OperationMode mode, boolean active, long licenseExpiryDate) {
this.mode = mode;
this.active = active;
this.licenseExpiryDate = licenseExpiryDate;
}
}

Expand All @@ -424,7 +434,7 @@ private static class Status {
// XPackLicenseState. However, if status is read multiple times in a method, it can change in between
// reads. Methods should use `executeAgainstStatus` and `checkAgainstStatus` to ensure that the status
// is only read once.
private volatile Status status = new Status(OperationMode.TRIAL, true);
private volatile Status status = new Status(OperationMode.TRIAL, true, Long.MAX_VALUE);

public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) {
this.listeners = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -472,12 +482,13 @@ private boolean checkAgainstStatus(Predicate<Status> statusPredicate) {
*
* @param mode The mode (type) of the current license.
* @param active True if the current license exists and is within its allowed usage period; false if it is expired or missing.
* @param expirationDate Expiration date of the current license.
* @param mostRecentTrialVersion If this cluster has, at some point commenced a trial, the most recent version on which they did that.
* May be {@code null} if they have never generated a trial license on this cluster, or the most recent
* trial was prior to this metadata being tracked (6.1)
*/
void update(OperationMode mode, boolean active, @Nullable Version mostRecentTrialVersion) {
status = new Status(mode, active);
void update(OperationMode mode, boolean active, long expirationDate, @Nullable Version mostRecentTrialVersion) {
status = new Status(mode, active, expirationDate);
listeners.forEach(LicenseStateListener::licenseStateChanged);
}

Expand Down Expand Up @@ -513,12 +524,26 @@ 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);
final long licenseExpiryDate = getLicenseExpiryDate();
final long diff = licenseExpiryDate - System.currentTimeMillis();
if (maxEpochAccumulator != null) {
maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong());
}

if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 &&
LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
final long days = TimeUnit.MILLISECONDS.toDays(diff);
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
HeaderWarning.addWarning("Your license {}. " +
"Contact your administrator or update your license for continued use of features", expiryMessage);
}

return allowed;
}

Expand Down Expand Up @@ -635,6 +660,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
});
}

/** Return the current license expiration date. */
public long getLicenseExpiryDate() {
return executeAgainstStatus(status -> status.licenseExpiryDate);
}

/**
* A convenient method to test whether a feature is by license status.
* @see #isAllowedByLicense(OperationMode, boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,21 +360,23 @@ public static class AssertingLicenseState extends XPackLicenseState {
public final List<License.OperationMode> modeUpdates = new ArrayList<>();
public final List<Boolean> activeUpdates = new ArrayList<>();
public final List<Version> trialVersionUpdates = new ArrayList<>();
public final List<Long> expirationDateUpdates = new ArrayList<>();

public AssertingLicenseState() {
super(Settings.EMPTY, () -> 0);
}

@Override
void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
modeUpdates.add(mode);
activeUpdates.add(active);
expirationDateUpdates.add(expirationDate);
trialVersionUpdates.add(mostRecentTrialVersion);
}
}

/**
* A license state that makes the {@link #update(License.OperationMode, boolean, Version)}
* A license state that makes the {@link #update(License.OperationMode, boolean, long, Version)}
* method public for use in tests.
*/
public static class UpdatableLicenseState extends XPackLicenseState {
Expand All @@ -387,8 +389,8 @@ public UpdatableLicenseState(Settings settings) {
}

@Override
public void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
super.update(mode, active, mostRecentTrialVersion);
public void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
super.update(mode, active, expirationDate, mostRecentTrialVersion);
}
}

Expand Down
Loading

0 comments on commit e0b75c9

Please sign in to comment.