Skip to content

Commit 0b586c2

Browse files
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]>
1 parent 1cf0a6e commit 0b586c2

File tree

18 files changed

+212
-57
lines changed

18 files changed

+212
-57
lines changed

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
@@ -81,6 +81,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
8181
*/
8282
static final TimeValue GRACE_PERIOD_DURATION = days(7);
8383

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

@@ -124,7 +129,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
124129

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

127-
private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
132+
public static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
128133

129134
private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
130135
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
@@ -475,7 +480,7 @@ private void updateLicenseState(LicensesMetadata licensesMetadata) {
475480
protected void updateLicenseState(final License license, Version mostRecentTrialVersion) {
476481
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
477482
// implies license has been explicitly deleted
478-
licenseState.update(License.OperationMode.MISSING, false, mostRecentTrialVersion);
483+
licenseState.update(License.OperationMode.MISSING, false, license.expiryDate(), mostRecentTrialVersion);
479484
return;
480485
}
481486
if (license != null) {
@@ -488,7 +493,7 @@ protected void updateLicenseState(final License license, Version mostRecentTrial
488493
// date that is near Long.MAX_VALUE
489494
active = time >= license.issueDate() && time - GRACE_PERIOD_DURATION.getMillis() < license.expiryDate();
490495
}
491-
licenseState.update(license.operationMode(), active, mostRecentTrialVersion);
496+
licenseState.update(license.operationMode(), active, license.expiryDate(), mostRecentTrialVersion);
492497

493498
if (active) {
494499
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
*/
@@ -395,7 +401,7 @@ private static boolean isBasic(OperationMode mode) {
395401
return mode == OperationMode.BASIC;
396402
}
397403

398-
/** A wrapper for the license mode and state, to allow atomically swapping. */
404+
/** A wrapper for the license mode, state, and expiration date, to allow atomically swapping. */
399405
private static class Status {
400406

401407
/** The current "mode" of the license (ie license type). */
@@ -404,9 +410,13 @@ private static class Status {
404410
/** True if the license is active, or false if it is expired. */
405411
final boolean active;
406412

407-
Status(OperationMode mode, boolean active) {
413+
/** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
414+
final long licenseExpiryDate;
415+
416+
Status(OperationMode mode, boolean active, long licenseExpiryDate) {
408417
this.mode = mode;
409418
this.active = active;
419+
this.licenseExpiryDate = licenseExpiryDate;
410420
}
411421
}
412422

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

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

@@ -509,12 +520,26 @@ public boolean isActive() {
509520
/**
510521
* Checks whether the given feature is allowed, tracking the last usage time.
511522
*/
523+
@SuppressForbidden(reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE")
512524
public boolean checkFeature(Feature feature) {
513525
boolean allowed = isAllowed(feature);
514526
LongAccumulator maxEpochAccumulator = lastUsed.get(feature);
527+
final long licenseExpiryDate = getLicenseExpiryDate();
528+
final long diff = licenseExpiryDate - System.currentTimeMillis();
515529
if (maxEpochAccumulator != null) {
516530
maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong());
517531
}
532+
533+
if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 &&
534+
LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
535+
final long days = TimeUnit.MILLISECONDS.toDays(diff);
536+
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
537+
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
538+
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
539+
HeaderWarning.addWarning("Your license {}. " +
540+
"Contact your administrator or update your license for continued use of features", expiryMessage);
541+
}
542+
518543
return allowed;
519544
}
520545

@@ -631,6 +656,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
631656
});
632657
}
633658

659+
/** Return the current license expiration date. */
660+
public long getLicenseExpiryDate() {
661+
return executeAgainstStatus(status -> status.licenseExpiryDate);
662+
}
663+
634664
/**
635665
* A convenient method to test whether a feature is by license status.
636666
* @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)