Skip to content

Commit e0b75c9

Browse files
Adding a warning header when a license is about to expire #64948 (#65900)
* 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 <[email protected]> * Resolving backporting issue: adding copyMapWithRemovedEntry() util function Fixing unused imports Co-authored-by: Elastic Machine <[email protected]>
1 parent 050c7eb commit e0b75c9

File tree

19 files changed

+246
-56
lines changed

19 files changed

+246
-56
lines changed

server/src/main/java/org/elasticsearch/common/util/Maps.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919

2020
package org.elasticsearch.common.util;
2121

22+
import org.elasticsearch.Assertions;
23+
24+
import java.util.Collections;
2225
import java.util.Map;
2326
import java.util.Objects;
27+
import java.util.stream.Collectors;
2428

2529
public class Maps {
2630

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

50+
/**
51+
* Remove the specified key from the provided immutable map by copying the underlying map and filtering out the specified
52+
* key if that key exists.
53+
*
54+
* @param map the immutable map to remove the key from
55+
* @param key the key to be removed
56+
* @param <K> the type of the keys in the map
57+
* @param <V> the type of the values in the map
58+
* @return an immutable map that contains the items from the specified map with the provided key removed
59+
*/
60+
public static <K, V> Map<K, V> copyMapWithRemovedEntry(final Map<K, V> map, final K key) {
61+
Objects.requireNonNull(map);
62+
Objects.requireNonNull(key);
63+
assertImmutableMap(map, key, map.get(key));
64+
return map.entrySet().stream().filter(k -> key.equals(k.getKey()) == false)
65+
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue),
66+
Collections::<K, V>unmodifiableMap));
67+
}
68+
69+
private static <K, V> void assertImmutableMap(final Map<K, V> map, final K key, final V value) {
70+
if (Assertions.ENABLED) {
71+
boolean immutable;
72+
try {
73+
map.put(key, value);
74+
immutable = false;
75+
} catch (final UnsupportedOperationException e) {
76+
immutable = true;
77+
}
78+
assert immutable : "expected an immutable map but was [" + map.getClass() + "]";
79+
}
80+
}
4681
}

server/src/main/java/org/elasticsearch/http/DefaultRestChannel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void sendResponse(RestResponse restResponse) {
123123

124124
// Add all custom headers
125125
addCustomHeaders(httpResponse, restResponse.getHeaders());
126-
addCustomHeaders(httpResponse, threadContext.getResponseHeaders());
126+
addCustomHeaders(httpResponse, restResponse.filterHeaders(threadContext.getResponseHeaders()));
127127

128128
// If our response doesn't specify a content-type header, set one
129129
setHeaderField(httpResponse, CONTENT_TYPE, restResponse.contentType(), false);

server/src/main/java/org/elasticsearch/rest/RestResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,8 @@ public Map<String, List<String>> getHeaders() {
8989
return customHeaders;
9090
}
9191
}
92+
93+
public Map<String, List<String>> filterHeaders(Map<String, List<String>> headers) {
94+
return headers;
95+
}
9296
}

x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
8282
*/
8383
static final TimeValue GRACE_PERIOD_DURATION = days(7);
8484

85+
/**
86+
* Period before the license expires when warning starts being added to the response header
87+
*/
88+
static final TimeValue LICENSE_EXPIRATION_WARNING_PERIOD = days(7);
89+
8590
public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS =
8691
XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
8792

@@ -125,7 +130,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
125130

126131
public static final String LICENSE_JOB = "licenseJob";
127132

128-
private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
133+
public static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
129134

130135
private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
131136
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
@@ -476,7 +481,7 @@ private void updateLicenseState(LicensesMetadata licensesMetadata) {
476481
protected void updateLicenseState(final License license, Version mostRecentTrialVersion) {
477482
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
478483
// implies license has been explicitly deleted
479-
licenseState.update(License.OperationMode.MISSING, false, mostRecentTrialVersion);
484+
licenseState.update(License.OperationMode.MISSING, false, license.expiryDate(), mostRecentTrialVersion);
480485
return;
481486
}
482487
if (license != null) {
@@ -489,7 +494,7 @@ protected void updateLicenseState(final License license, Version mostRecentTrial
489494
// date that is near Long.MAX_VALUE
490495
active = time >= license.issueDate() && time - GRACE_PERIOD_DURATION.getMillis() < license.expiryDate();
491496
}
492-
licenseState.update(license.operationMode(), active, mostRecentTrialVersion);
497+
licenseState.update(license.operationMode(), active, license.expiryDate(), mostRecentTrialVersion);
493498

494499
if (active) {
495500
if (time < license.expiryDate()) {

x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseStateListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
public interface LicenseStateListener {
1616

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

x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.elasticsearch.Version;
99
import org.elasticsearch.common.Nullable;
1010
import org.elasticsearch.common.Strings;
11+
import org.elasticsearch.common.SuppressForbidden;
12+
import org.elasticsearch.common.logging.HeaderWarning;
1113
import org.elasticsearch.common.logging.LoggerMessageFormat;
1214
import org.elasticsearch.common.settings.Settings;
1315
import org.elasticsearch.license.License.OperationMode;
@@ -19,17 +21,21 @@
1921
import java.util.EnumMap;
2022
import java.util.LinkedHashMap;
2123
import java.util.List;
24+
import java.util.Locale;
2225
import java.util.Map;
2326
import java.util.Objects;
2427
import java.util.Set;
2528
import java.util.concurrent.CopyOnWriteArrayList;
29+
import java.util.concurrent.TimeUnit;
2630
import java.util.concurrent.atomic.LongAccumulator;
2731
import java.util.function.BiFunction;
2832
import java.util.function.Function;
2933
import java.util.function.LongSupplier;
3034
import java.util.function.Predicate;
3135
import java.util.stream.Collectors;
3236

37+
import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;
38+
3339
/**
3440
* A holder for the current state of the license for all xpack features.
3541
*/
@@ -399,7 +405,7 @@ private static boolean isBasic(OperationMode mode) {
399405
return mode == OperationMode.BASIC;
400406
}
401407

402-
/** A wrapper for the license mode and state, to allow atomically swapping. */
408+
/** A wrapper for the license mode, state, and expiration date, to allow atomically swapping. */
403409
private static class Status {
404410

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

411-
Status(OperationMode mode, boolean active) {
417+
/** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
418+
final long licenseExpiryDate;
419+
420+
Status(OperationMode mode, boolean active, long licenseExpiryDate) {
412421
this.mode = mode;
413422
this.active = active;
423+
this.licenseExpiryDate = licenseExpiryDate;
414424
}
415425
}
416426

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

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

@@ -513,12 +524,26 @@ boolean isActive() {
513524
/**
514525
* Checks whether the given feature is allowed, tracking the last usage time.
515526
*/
527+
@SuppressForbidden(reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE")
516528
public boolean checkFeature(Feature feature) {
517529
boolean allowed = isAllowed(feature);
518530
LongAccumulator maxEpochAccumulator = lastUsed.get(feature);
531+
final long licenseExpiryDate = getLicenseExpiryDate();
532+
final long diff = licenseExpiryDate - System.currentTimeMillis();
519533
if (maxEpochAccumulator != null) {
520534
maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong());
521535
}
536+
537+
if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 &&
538+
LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
539+
final long days = TimeUnit.MILLISECONDS.toDays(diff);
540+
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
541+
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
542+
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
543+
HeaderWarning.addWarning("Your license {}. " +
544+
"Contact your administrator or update your license for continued use of features", expiryMessage);
545+
}
546+
522547
return allowed;
523548
}
524549

@@ -635,6 +660,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
635660
});
636661
}
637662

663+
/** Return the current license expiration date. */
664+
public long getLicenseExpiryDate() {
665+
return executeAgainstStatus(status -> status.licenseExpiryDate);
666+
}
667+
638668
/**
639669
* A convenient method to test whether a feature is by license status.
640670
* @see #isAllowedByLicense(OperationMode, boolean)

x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,21 +360,23 @@ public static class AssertingLicenseState extends XPackLicenseState {
360360
public final List<License.OperationMode> modeUpdates = new ArrayList<>();
361361
public final List<Boolean> activeUpdates = new ArrayList<>();
362362
public final List<Version> trialVersionUpdates = new ArrayList<>();
363+
public final List<Long> expirationDateUpdates = new ArrayList<>();
363364

364365
public AssertingLicenseState() {
365366
super(Settings.EMPTY, () -> 0);
366367
}
367368

368369
@Override
369-
void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
370+
void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
370371
modeUpdates.add(mode);
371372
activeUpdates.add(active);
373+
expirationDateUpdates.add(expirationDate);
372374
trialVersionUpdates.add(mostRecentTrialVersion);
373375
}
374376
}
375377

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

389391
@Override
390-
public void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
391-
super.update(mode, active, mostRecentTrialVersion);
392+
public void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
393+
super.update(mode, active, expirationDate, mostRecentTrialVersion);
392394
}
393395
}
394396

0 commit comments

Comments
 (0)