Skip to content

Commit

Permalink
[7.17] Enable overrides of license expiration dates (#85351) (#85687)
Browse files Browse the repository at this point in the history
* Enable overrides of license expiration dates (#85351)

In rare cases, we are required to adjust the expiration date
of a license that already exists for legal or accounting reasons,
rather than simply generating a new one. This commit introduces
a basic mechanism for overriding licenses by hash of license ID, by
way of a list currently hardcoded in the repo.

This approach is fairly naive, and should be replaced as noted in the
code if the frequency we are required to use this mechanism increases.

This PR adds Java-based REST tests for the Put License API to help
validate this behavior.

(cherry picked from commit 4ea2103)

* Fix compilation for 7.17 branch
  • Loading branch information
gwbrown committed Apr 5, 2022
1 parent acf13e0 commit 8578c5e
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.protocol.xpack.license.LicenseStatus;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.ToXContentObject;
Expand Down Expand Up @@ -332,6 +331,9 @@ public long startDate() {
}

/**
* The expiration date as it appears in the license. For most uses, prefer {@link LicenseService#getExpiryDate(License)}, as in
* rare cases the effective expiration date may differ from the expiration date specified in the license.
*
* @return the expiry date in milliseconds
*/
public long expiryDate() {
Expand Down Expand Up @@ -412,19 +414,6 @@ public synchronized void removeOperationModeFileWatcher() {
this.operationModeFileWatcher = null;
}

/**
* @return the current license's status
*/
public LicenseStatus status() {
long now = System.currentTimeMillis();
if (issueDate > now) {
return LicenseStatus.INVALID;
} else if (expiryDate < now) {
return LicenseStatus.EXPIRED;
}
return LicenseStatus.ACTIVE;
}

private void validate() {
if (issuer == null) {
throw new IllegalStateException("issuer can not be null");
Expand Down Expand Up @@ -562,7 +551,7 @@ public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) t
licenseVersion = this.version;
}
if (restViewMode) {
builder.field(Fields.STATUS, status().label());
builder.field(Fields.STATUS, LicenseService.status(this).label());
}
builder.field(Fields.UID, uid);

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.component.Lifecycle;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -31,6 +32,7 @@
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest;
import org.elasticsearch.protocol.xpack.license.LicenseStatus;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.threadpool.ThreadPool;
Expand All @@ -39,6 +41,7 @@
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.scheduler.SchedulerEngine;

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -72,7 +75,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
Setting.Property.NodeScope
);

static final List<License.LicenseType> ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes();
public static final List<License.LicenseType> ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes();

public static final Setting<List<License.LicenseType>> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting(
"xpack.license.upload.types",
Expand Down Expand Up @@ -213,17 +216,40 @@ private void populateExpirationCallbacks() {
expirationCallbacks.add(new ExpirationCallback.Pre(days(0), days(25), days(1)) {
@Override
public void on(License license) {
logExpirationWarning(license.expiryDate(), false);
logExpirationWarning(getExpiryDate(license), false);
}
});
expirationCallbacks.add(new ExpirationCallback.Post(days(0), null, TimeValue.timeValueMinutes(10)) {
@Override
public void on(License license) {
logExpirationWarning(license.expiryDate(), true);
logExpirationWarning(getExpiryDate(license), true);
}
});
}

/**
* Gets the effective expiry date of the given license, including any overrides.
*/
public static long getExpiryDate(License license) {
String licenseUidHash = MessageDigests.toHexString(MessageDigests.sha256().digest(license.uid().getBytes(StandardCharsets.UTF_8)));
return LicenseOverrides.overrideDateForLicense(licenseUidHash)
.map(date -> date.toInstant().toEpochMilli())
.orElse(license.expiryDate());
}

/**
* Gets the current status of a license
*/
public static LicenseStatus status(License license) {
long now = System.currentTimeMillis();
if (license.issueDate() > now) {
return LicenseStatus.INVALID;
} else if (LicenseService.getExpiryDate(license) < now) {
return LicenseStatus.EXPIRED;
}
return LicenseStatus.ACTIVE;
}

/**
* Registers new license in the cluster
* Master only operation. Installs a new license on the master provided it is VALID
Expand All @@ -248,7 +274,7 @@ public void registerLicense(final PutLicenseRequest request, final ActionListene
listener.onFailure(
new IllegalArgumentException("Registering [" + licenseType.getTypeName() + "] licenses is not allowed on this cluster")
);
} else if (newLicense.expiryDate() < now) {
} else if (getExpiryDate(newLicense) < now) {
listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED));
} else {
if (request.acknowledged() == false) {
Expand Down Expand Up @@ -536,17 +562,17 @@ protected void updateLicenseState(final License license) {
long time = clock.millis();
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
// implies license has been explicitly deleted
licenseState.update(License.OperationMode.MISSING, false, getExpiryWarning(license.expiryDate(), time));
licenseState.update(License.OperationMode.MISSING, false, getExpiryWarning(getExpiryDate(license), time));
return;
}
if (license != null) {
final boolean active;
if (license.expiryDate() == BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
if (getExpiryDate(license) == BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
active = true;
} else {
active = time >= license.issueDate() && time < license.expiryDate();
active = time >= license.issueDate() && time < getExpiryDate(license);
}
licenseState.update(license.operationMode(), active, getExpiryWarning(license.expiryDate(), time));
licenseState.update(license.operationMode(), active, getExpiryWarning(getExpiryDate(license), time));

if (active) {
logger.debug("license [{}] - valid", license.uid());
Expand Down Expand Up @@ -576,7 +602,7 @@ private void onUpdate(final LicensesMetadata currentLicensesMetadata) {
scheduler.add(
new SchedulerEngine.Job(
expirationCallback.getId(),
(startTime, now) -> expirationCallback.nextScheduledTimeForExpiry(license.expiryDate(), startTime, now)
(startTime, now) -> expirationCallback.nextScheduledTimeForExpiry(getExpiryDate(license), startTime, now)
)
);
}
Expand All @@ -600,10 +626,10 @@ static SchedulerEngine.Schedule nextLicenseCheck(License license) {
// so the license is notified once it is valid
// see https://github.com/elastic/x-plugins/issues/983
return license.issueDate();
} else if (time < license.expiryDate()) {
} else if (time < getExpiryDate(license)) {
// Re-check the license every day during the warning period up to the license expiration.
// This will cause the warning message to be updated that is emitted on soon-expiring license use.
long nextTime = license.expiryDate() - LICENSE_EXPIRATION_WARNING_PERIOD.getMillis();
long nextTime = getExpiryDate(license) - LICENSE_EXPIRATION_WARNING_PERIOD.getMillis();
while (nextTime <= time) {
nextTime += TimeValue.timeValueDays(1).getMillis();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static boolean isLicenseExpiredException(ElasticsearchSecurityException e
}

public static boolean licenseNeedsExtended(License license) {
return LicenseType.isBasic(license.type()) && license.expiryDate() != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
return LicenseType.isBasic(license.type())
&& LicenseService.getExpiryDate(license) != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private boolean shouldGenerateNewBasicLicense(License currentLicense) {
return currentLicense == null
|| License.LicenseType.isBasic(currentLicense.type()) == false
|| LicenseService.SELF_GENERATED_LICENSE_MAX_NODES != currentLicense.maxNodes()
|| LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS != currentLicense.expiryDate();
|| LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS != LicenseService.getExpiryDate(currentLicense);
}

private License generateBasicLicense(ClusterState currentState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private ClusterState updateLicenseSignature(ClusterState currentState, LicensesM
Metadata.Builder mdBuilder = Metadata.builder(currentState.metadata());
String type = license.type();
long issueDate = license.issueDate();
long expiryDate = license.expiryDate();
long expiryDate = LicenseService.getExpiryDate(license);
// extend the basic license expiration date if needed since extendBasic will not be called now
if (License.LicenseType.isBasic(type) && expiryDate != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
expiryDate = LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ protected void doExecute(Task task, XPackInfoRequest request, ActionListener<XPa
mode = License.OperationMode.PLATINUM;
}
}
licenseInfo = new LicenseInfo(license.uid(), type, mode.description(), license.status(), license.expiryDate());
licenseInfo = new LicenseInfo(
license.uid(),
type,
mode.description(),
LicenseService.status(license),
LicenseService.getExpiryDate(license)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -114,6 +115,31 @@ public void testFailToRegisterLicenseNotMatchingTypeRestrictions() throws Except
assertRegisterDisallowedLicenseType(settings, notAllowed);
}

/**
* Tests that the license overrides from {@link LicenseOverrides} are applied when an override is present for a license's ID.
*/
public void testLicenseExpiryDateOverride() throws IOException {
UUID licenseId = UUID.fromString("12345678-abcd-0000-0000-000000000000"); // Special test UUID
License.LicenseType type = randomFrom(License.LicenseType.values());
License testLicense = buildLicense(licenseId, type, TimeValue.timeValueDays(randomIntBetween(1, 100)).millis());

assertThat(LicenseService.getExpiryDate(testLicense), equalTo(new Date(42000L).getTime()));
}

/**
* Tests that a license with an overridden expiry date that's in the past is expired.
*/
public void testLicenseWithOverridenExpiryInPastIsExpired() throws IOException {
UUID licenseId = UUID.fromString("12345678-abcd-0000-0000-000000000000"); // Special test UUID
License.LicenseType type = randomFrom(LicenseService.ALLOWABLE_UPLOAD_TYPES);
License testLicense = sign(buildLicense(licenseId, type, TimeValue.timeValueDays(randomIntBetween(1, 100)).millis()));

tryRegisterLicense(Settings.EMPTY, testLicense, future -> {
PutLicenseResponse response = future.actionGet();
assertThat(response.status(), equalTo(LicensesStatus.EXPIRED));
});
}

private void assertRegisterValidLicense(Settings baseSettings, License.LicenseType licenseType) throws IOException {
tryRegisterLicense(baseSettings, licenseType, future -> assertThat(future.actionGet().status(), equalTo(LicensesStatus.VALID)));
}
Expand All @@ -135,6 +161,11 @@ private void tryRegisterLicense(
License.LicenseType licenseType,
Consumer<PlainActionFuture<PutLicenseResponse>> assertion
) throws IOException {
tryRegisterLicense(baseSettings, sign(buildLicense(licenseType, TimeValue.timeValueDays(randomLongBetween(1, 1000)))), assertion);
}

private void tryRegisterLicense(Settings baseSettings, License license, Consumer<PlainActionFuture<PutLicenseResponse>> assertion)
throws IOException {
final Settings settings = Settings.builder()
.put(baseSettings)
.put("path.home", createTempDir())
Expand Down Expand Up @@ -163,7 +194,7 @@ private void tryRegisterLicense(
);

final PutLicenseRequest request = new PutLicenseRequest();
request.license(spec(licenseType, TimeValue.timeValueDays(randomLongBetween(1, 1000))), XContentType.JSON);
request.license(toSpec(license), XContentType.JSON);
final PlainActionFuture<PutLicenseResponse> future = new PlainActionFuture<>();
service.registerLicense(request, future);

Expand All @@ -182,11 +213,6 @@ private void tryRegisterLicense(
}
}

private BytesReference spec(License.LicenseType type, TimeValue expires) throws IOException {
final License signed = sign(buildLicense(type, expires));
return toSpec(signed);
}

private BytesReference toSpec(License license) throws IOException {
XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON);
builder.startObject();
Expand All @@ -207,10 +233,14 @@ private License sign(License license) throws IOException {
}

private License buildLicense(License.LicenseType type, TimeValue expires) {
return buildLicense(new UUID(randomLong(), randomLong()), type, expires.millis());
}

private License buildLicense(UUID licenseId, License.LicenseType type, long expires) {
return License.builder()
.uid(new UUID(randomLong(), randomLong()).toString())
.uid(licenseId.toString())
.type(type)
.expiryDate(System.currentTimeMillis() + expires.millis())
.expiryDate(System.currentTimeMillis() + expires)
.issuer(randomAlphaOfLengthBetween(5, 60))
.issuedTo(randomAlphaOfLengthBetween(5, 60))
.issueDate(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(randomLongBetween(1, 5000)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
import org.elasticsearch.protocol.xpack.license.LicenseStatus;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
Expand Down Expand Up @@ -54,8 +53,6 @@ public void testDoExecute() throws Exception {
License license = mock(License.class);
long expiryDate = randomLong();
when(license.expiryDate()).thenReturn(expiryDate);
LicenseStatus status = randomFrom(LicenseStatus.values());
when(license.status()).thenReturn(status);
String licenseType = randomAlphaOfLength(10);
when(license.type()).thenReturn(licenseType);
License.OperationMode licenseMode = randomFrom(License.OperationMode.values());
Expand All @@ -65,7 +62,6 @@ public void testDoExecute() throws Exception {

checkAction(categories, -1, license, (XPackInfoResponse.LicenseInfo licenseInfo) -> {
assertThat(licenseInfo.getExpiryDate(), is(expiryDate));
assertThat(licenseInfo.getStatus(), is(status));
assertThat(licenseInfo.getType(), is(licenseType));
assertThat(licenseInfo.getMode(), is(licenseMode.name().toLowerCase(Locale.ROOT)));
assertThat(licenseInfo.getUid(), is(uid));
Expand All @@ -77,7 +73,6 @@ public void testDoExecuteWithEnterpriseLicenseWithoutBackwardsCompat() throws Ex

License license = mock(License.class);
when(license.expiryDate()).thenReturn(randomLong());
when(license.status()).thenReturn(LicenseStatus.ACTIVE);
when(license.type()).thenReturn("enterprise");
when(license.operationMode()).thenReturn(License.OperationMode.ENTERPRISE);
when(license.uid()).thenReturn(randomAlphaOfLength(30));
Expand All @@ -93,7 +88,6 @@ public void testDoExecuteWithEnterpriseLicenseWithBackwardsCompat() throws Excep

License license = mock(License.class);
when(license.expiryDate()).thenReturn(randomLong());
when(license.status()).thenReturn(LicenseStatus.ACTIVE);
when(license.type()).thenReturn("enterprise");
when(license.operationMode()).thenReturn(License.OperationMode.ENTERPRISE);
when(license.uid()).thenReturn(randomAlphaOfLength(30));
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugin/data-streams/qa/multi-node/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ testClusters.matching { it.name == "javaRestTest" }.configureEach {
setting 'indices.lifecycle.history_index_enabled', 'false'
}

dependencies {
javaRestTestImplementation(testArtifact(project(xpackModule('core'))))
}

if (BuildParams.inFipsJvm){
// Test clusters run with security disabled
tasks.named("javaRestTest").configure{enabled = false }
Expand Down

0 comments on commit 8578c5e

Please sign in to comment.