From 3b688bfee5ac207b660ca33994e366fa7b2cedd6 Mon Sep 17 00:00:00 2001
From: Ryan Ernst <ryan@elastic.co>
Date: Tue, 14 Jul 2020 14:34:59 -0700
Subject: [PATCH] Add license feature usage api (#59342) (#59571)

This commit adds a new api to track when gold+ features are used within
x-pack. The tracking is done internally whenever a feature is checked
against the current license. The output of the api is a list of each
used feature, which includes the name, license level, and last time it
was used. In addition to a unit test for the tracking, a rest test is
added which ensures starting up a default configured node does not
result in any features registering as used.

There are a couple features which currently do not work well with the
tracking, as they are checked in a manner that makes them look always
used. Those features will be fixed in followups, and in this PR they are
omitted from the feature usage output.
---
 .../common/MemoizedSupplier.java              | 40 +++++++++
 x-pack/plugin/core/build.gradle               | 22 ++++-
 .../license/GetFeatureUsageRequest.java       | 27 ++++++
 .../license/GetFeatureUsageResponse.java      | 84 +++++++++++++++++++
 .../org/elasticsearch/license/Licensing.java  |  4 +-
 .../license/RestGetFeatureUsageAction.java    | 37 ++++++++
 .../TransportGetFeatureUsageAction.java       | 53 ++++++++++++
 .../license/XPackLicenseState.java            | 53 ++++++++++--
 .../elasticsearch/xpack/core/XPackPlugin.java | 11 ++-
 .../license/LicenseFIPSTests.java             |  6 +-
 .../license/LicenseTLSTests.java              | 10 +--
 .../org/elasticsearch/license/TestUtils.java  |  6 +-
 .../license/XPackLicenseStateTests.java       | 45 +++++++---
 .../core/LocalStateCompositeXPackPlugin.java  | 11 +++
 .../XPackCoreClientYamlTestSuiteIT.java       | 39 +++++++++
 .../api/license.get_feature_usage.json        | 17 ++++
 .../test/license/10_feature_usage.yml         |  6 ++
 .../xpack/ml/InvalidLicenseEnforcer.java      |  2 +-
 .../ml/MachineLearningFeatureSetTests.java    |  2 +-
 .../xpack/security/Security.java              |  9 +-
 .../BulkShardRequestInterceptor.java          |  6 +-
 ...cumentLevelSecurityRequestInterceptor.java |  6 +-
 .../IndicesAliasesRequestInterceptor.java     | 35 ++++----
 .../interceptor/ResizeRequestInterceptor.java | 27 +++---
 .../authz/store/CompositeRolesStore.java      | 12 +--
 .../security/authz/store/FileRolesStore.java  |  6 +-
 .../authz/store/NativeRolesStore.java         | 45 +++++-----
 .../logfile/LoggingAuditTrailFilterTests.java |  2 +-
 .../audit/logfile/LoggingAuditTrailTests.java |  2 +-
 .../authz/AuthorizationServiceTests.java      |  4 +-
 .../authz/store/CompositeRolesStoreTests.java | 28 +++----
 .../transport/ServerTransportFilterTests.java |  4 +-
 32 files changed, 538 insertions(+), 123 deletions(-)
 create mode 100644 libs/core/src/main/java/org/elasticsearch/common/MemoizedSupplier.java
 create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageRequest.java
 create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageResponse.java
 create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetFeatureUsageAction.java
 create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportGetFeatureUsageAction.java
 create mode 100644 x-pack/plugin/core/src/yamlRestTest/java/org/elasticsearch/license/XPackCoreClientYamlTestSuiteIT.java
 create mode 100644 x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/api/license.get_feature_usage.json
 create mode 100644 x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/test/license/10_feature_usage.yml

diff --git a/libs/core/src/main/java/org/elasticsearch/common/MemoizedSupplier.java b/libs/core/src/main/java/org/elasticsearch/common/MemoizedSupplier.java
new file mode 100644
index 00000000000..d010eac5205
--- /dev/null
+++ b/libs/core/src/main/java/org/elasticsearch/common/MemoizedSupplier.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common;
+
+import java.util.function.Supplier;
+
+public class MemoizedSupplier<T> implements Supplier<T> {
+    private Supplier<T> supplier;
+    private T value;
+
+    public MemoizedSupplier(Supplier<T> supplier) {
+        this.supplier = supplier;
+    }
+
+    @Override
+    public T get() {
+        if (supplier != null) {
+            value = supplier.get();
+            supplier = null;
+        }
+        return value;
+    }
+}
diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle
index ff84ac5ad89..a77cd87c149 100644
--- a/x-pack/plugin/core/build.gradle
+++ b/x-pack/plugin/core/build.gradle
@@ -7,6 +7,7 @@ import java.nio.file.Paths
 apply plugin: 'elasticsearch.esplugin'
 apply plugin: 'elasticsearch.publish'
 apply plugin: 'elasticsearch.internal-cluster-test'
+apply plugin: 'elasticsearch.yaml-rest-test'
 
 archivesBaseName = 'x-pack-core'
 
@@ -57,6 +58,8 @@ dependencies {
     transitive = false
   }
 
+  yamlRestTestImplementation project(':x-pack:plugin:core')
+
 }
 
 ext.expansions = [
@@ -143,7 +146,18 @@ thirdPartyAudit.ignoreMissingClasses(
   'javax.servlet.ServletContextListener'
 )
 
-// xpack modules are installed in real clusters as the meta plugin, so
-// installing them as individual plugins for integ tests doesn't make sense,
-// so we disable integ tests
-integTest.enabled = false
+restResources {
+  restApi {
+    includeCore '*'
+  }
+}
+
+testClusters.yamlRestTest {
+  testDistribution = 'default'
+  setting 'xpack.security.enabled', 'true'
+  setting 'xpack.license.self_generated.type', 'trial'
+  keystore 'bootstrap.password', 'x-pack-test-password'
+  user username: "x_pack_rest_user", password: "x-pack-test-password"
+}
+
+testingConventions.enabled = false
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageRequest.java
new file mode 100644
index 00000000000..d8bddd86cc3
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageRequest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.license;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.StreamInput;
+
+import java.io.IOException;
+
+public class GetFeatureUsageRequest extends ActionRequest {
+
+    public GetFeatureUsageRequest() {}
+
+    public GetFeatureUsageRequest(StreamInput in) throws IOException {
+        super(in);
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageResponse.java
new file mode 100644
index 00000000000..335190d6bc4
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/GetFeatureUsageResponse.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.license;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Collections;
+import java.util.List;
+
+public class GetFeatureUsageResponse extends ActionResponse implements ToXContentObject {
+
+    public static class FeatureUsageInfo implements Writeable {
+        public final String name;
+        public final ZonedDateTime lastUsedTime;
+        public final String licenseLevel;
+
+        public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, String licenseLevel) {
+            this.name = name;
+            this.lastUsedTime = lastUsedTime;
+            this.licenseLevel = licenseLevel;
+        }
+
+        public FeatureUsageInfo(StreamInput in) throws IOException {
+            this.name = in.readString();
+            this.lastUsedTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(in.readLong()), ZoneOffset.UTC);
+            this.licenseLevel = in.readString();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeString(name);
+            out.writeLong(lastUsedTime.toEpochSecond());
+            out.writeString(licenseLevel);
+        }
+    }
+
+    private List<FeatureUsageInfo> features;
+
+    public GetFeatureUsageResponse(List<FeatureUsageInfo> features) {
+        this.features = Collections.unmodifiableList(features);
+    }
+
+    public GetFeatureUsageResponse(StreamInput in) throws IOException {
+        this.features = in.readList(FeatureUsageInfo::new);
+    }
+
+    public List<FeatureUsageInfo> getFeatures() {
+        return features;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeList(features);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.startArray("features");
+        for (FeatureUsageInfo feature : features) {
+            builder.startObject();
+            builder.field("name", feature.name);
+            builder.field("last_used", feature.lastUsedTime.toString());
+            builder.field("license_level", feature.licenseLevel);
+            builder.endObject();
+        }
+        builder.endArray();
+        builder.endObject();
+        return builder;
+    }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/Licensing.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/Licensing.java
index 295f8d8cf9e..1d9cb73a896 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/Licensing.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/Licensing.java
@@ -66,7 +66,8 @@ public class Licensing implements ActionPlugin {
                 new ActionHandler<>(PostStartTrialAction.INSTANCE, TransportPostStartTrialAction.class),
                 new ActionHandler<>(GetTrialStatusAction.INSTANCE, TransportGetTrialStatusAction.class),
                 new ActionHandler<>(PostStartBasicAction.INSTANCE, TransportPostStartBasicAction.class),
-                new ActionHandler<>(GetBasicStatusAction.INSTANCE, TransportGetBasicStatusAction.class));
+                new ActionHandler<>(GetBasicStatusAction.INSTANCE, TransportGetBasicStatusAction.class),
+                new ActionHandler<>(TransportGetFeatureUsageAction.TYPE, TransportGetFeatureUsageAction.class));
     }
 
     @Override
@@ -81,6 +82,7 @@ public class Licensing implements ActionPlugin {
         handlers.add(new RestGetBasicStatus());
         handlers.add(new RestPostStartTrialLicense());
         handlers.add(new RestPostStartBasicLicense());
+        handlers.add(new RestGetFeatureUsageAction());
         return handlers;
     }
 
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetFeatureUsageAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetFeatureUsageAction.java
new file mode 100644
index 00000000000..dd4cc4feab9
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RestGetFeatureUsageAction.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.license;
+
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static org.elasticsearch.rest.RestRequest.Method.GET;
+
+public class RestGetFeatureUsageAction extends BaseRestHandler {
+
+    @Override
+    public String getName() {
+        return "get_feature_usage";
+    }
+
+    @Override
+    public List<Route> routes() {
+        return Collections.singletonList(new Route(GET, "/_license/feature_usage"));
+    }
+
+    @Override
+    protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
+        return channel -> client.execute(TransportGetFeatureUsageAction.TYPE, new GetFeatureUsageRequest(),
+            new RestToXContentListener<>(channel));
+    }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportGetFeatureUsageAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportGetFeatureUsageAction.java
new file mode 100644
index 00000000000..b0dd78f94b4
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportGetFeatureUsageAction.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.license;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.transport.TransportService;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class TransportGetFeatureUsageAction extends HandledTransportAction<GetFeatureUsageRequest, GetFeatureUsageResponse> {
+
+    public static final ActionType<GetFeatureUsageResponse> TYPE =
+        new ActionType<>("cluster:admin/xpack/license/feature_usage", GetFeatureUsageResponse::new);
+
+    private final XPackLicenseState licenseState;
+
+    @Inject
+    public TransportGetFeatureUsageAction(TransportService transportService, ActionFilters actionFilters,
+                                          XPackLicenseState licenseState) {
+        super(TYPE.name(), transportService, actionFilters, GetFeatureUsageRequest::new);
+        this.licenseState = licenseState;
+    }
+
+
+    @Override
+    protected void doExecute(Task task, GetFeatureUsageRequest request, ActionListener<GetFeatureUsageResponse> listener) {
+        Map<XPackLicenseState.Feature, Long> featureUsage = licenseState.getLastUsed();
+        List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>();
+        for (Map.Entry<XPackLicenseState.Feature, Long> entry : featureUsage.entrySet()) {
+            XPackLicenseState.Feature feature = entry.getKey();
+            String name = feature.name().toLowerCase(Locale.ROOT);
+            ZonedDateTime lastUsedTime = Instant.ofEpochMilli(entry.getValue()).atZone(ZoneOffset.UTC);
+            String licenseLevel = feature.minimumOperationMode.name().toLowerCase(Locale.ROOT);
+            usageInfos.add(new GetFeatureUsageResponse.FeatureUsageInfo(name, lastUsedTime, licenseLevel));
+        }
+        listener.onResponse(new GetFeatureUsageResponse(usageInfos));
+    }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
index cb8c894d6c9..c8d07a3e58a 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
@@ -16,14 +16,19 @@ import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.monitoring.MonitoringField;
 
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
+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;
 
 /**
  * A holder for the current state of the license for all xpack features.
@@ -106,6 +111,14 @@ public class XPackLicenseState {
         }
     }
 
+    // temporarily non tracked feeatures which need rework in how they are checked
+    // so they are not tracked as always used
+    private static final Set<Feature> NON_TRACKED_FEATURES = org.elasticsearch.common.collect.Set.of(
+        Feature.SECURITY_IP_FILTERING,
+        Feature.SECURITY_ALL_REALMS,
+        Feature.SECURITY_STANDARD_REALMS
+    );
+
     /** Messages for each feature which are printed when the license expires. */
     static final Map<String, String[]> EXPIRATION_MESSAGES;
     static {
@@ -398,6 +411,8 @@ public class XPackLicenseState {
     private final List<LicenseStateListener> listeners;
     private final boolean isSecurityEnabled;
     private final boolean isSecurityExplicitlyEnabled;
+    private final Map<Feature, LongAccumulator> lastUsed;
+    private final LongSupplier epochMillisProvider;
 
     // Since Status is the only field that can be updated, we do not need to synchronize access to
     // XPackLicenseState. However, if status is read multiple times in a method, it can change in between
@@ -405,18 +420,31 @@ public class XPackLicenseState {
     // is only read once.
     private volatile Status status = new Status(OperationMode.TRIAL, true);
 
-    public XPackLicenseState(Settings settings) {
+    public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) {
         this.listeners = new CopyOnWriteArrayList<>();
         this.isSecurityEnabled = XPackSettings.SECURITY_ENABLED.get(settings);
         this.isSecurityExplicitlyEnabled = isSecurityEnabled && isSecurityExplicitlyEnabled(settings);
+
+        // prepopulate feature last used map with entries for non basic features, which are the ones we
+        // care to actually keep track of
+        Map<Feature, LongAccumulator> lastUsed = new EnumMap<>(Feature.class);
+        for (Feature feature : Feature.values()) {
+            if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 && NON_TRACKED_FEATURES.contains(feature) == false) {
+                lastUsed.put(feature, new LongAccumulator(Long::max, 0));
+            }
+        }
+        this.lastUsed = lastUsed;
+        this.epochMillisProvider = epochMillisProvider;
     }
 
     private XPackLicenseState(List<LicenseStateListener> listeners, boolean isSecurityEnabled, boolean isSecurityExplicitlyEnabled,
-                              Status status) {
+                              Status status, Map<Feature, LongAccumulator> lastUsed, LongSupplier epochMillisProvider) {
         this.listeners = listeners;
         this.isSecurityEnabled = isSecurityEnabled;
         this.isSecurityExplicitlyEnabled = isSecurityExplicitlyEnabled;
         this.status = status;
+        this.lastUsed = lastUsed;
+        this.epochMillisProvider = epochMillisProvider;
     }
 
     private static boolean isSecurityExplicitlyEnabled(Settings settings) {
@@ -480,8 +508,12 @@ public class XPackLicenseState {
      * Checks whether the given feature is allowed, tracking the last usage time.
      */
     public boolean checkFeature(Feature feature) {
-        // TODO: usage tracking is not yet implemented
-        return isAllowed(feature);
+        boolean allowed = isAllowed(feature);
+        LongAccumulator maxEpochAccumulator = lastUsed.get(feature);
+        if (maxEpochAccumulator != null) {
+            maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong());
+        }
+        return allowed;
     }
 
     /**
@@ -493,6 +525,17 @@ public class XPackLicenseState {
         return isAllowedByLicense(feature.minimumOperationMode, feature.needsActive);
     }
 
+    /**
+     * Returns a mapping of gold+ features to the last time that feature was used.
+     *
+     * Note that if a feature has not been used, it will not appear in the map.
+     */
+    public Map<Feature, Long> getLastUsed() {
+        return lastUsed.entrySet().stream()
+            .filter(e -> e.getValue().get() != 0) // feature was never used
+            .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
+    }
+
     public static boolean isMachineLearningAllowedForOperationMode(final OperationMode operationMode) {
         return isAllowedByOperationMode(operationMode, OperationMode.PLATINUM);
     }
@@ -566,7 +609,7 @@ public class XPackLicenseState {
      */
     public XPackLicenseState copyCurrentLicenseState() {
         return executeAgainstStatus(status ->
-            new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status));
+            new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider));
     }
 
     /**
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java
index ef0e80faf74..6b8ae63de5b 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java
@@ -85,6 +85,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
@@ -136,6 +137,7 @@ public class XPackPlugin extends XPackClientPlugin implements ExtensiblePlugin,
     private static final SetOnce<XPackLicenseState> licenseState = new SetOnce<>();
     private static final SetOnce<SSLService> sslService = new SetOnce<>();
     private static final SetOnce<LicenseService> licenseService = new SetOnce<>();
+    private static final SetOnce<LongSupplier> epochMillisSupplier = new SetOnce<>();
 
     public XPackPlugin(
             final Settings settings,
@@ -146,7 +148,7 @@ public class XPackPlugin extends XPackClientPlugin implements ExtensiblePlugin,
         this.settings = settings;
         this.transportClientMode = transportClientMode(settings);
 
-        setLicenseState(new XPackLicenseState(settings));
+        setLicenseState(new XPackLicenseState(settings, () -> getEpochMillisSupplier().getAsLong()));
 
         this.licensing = new Licensing(settings);
     }
@@ -159,9 +161,13 @@ public class XPackPlugin extends XPackClientPlugin implements ExtensiblePlugin,
     protected SSLService getSslService() { return getSharedSslService(); }
     protected LicenseService getLicenseService() { return getSharedLicenseService(); }
     protected XPackLicenseState getLicenseState() { return getSharedLicenseState(); }
+    protected LongSupplier getEpochMillisSupplier() { return getSharedEpochMillisSupplier(); }
     protected void setSslService(SSLService sslService) { XPackPlugin.sslService.set(sslService); }
     protected void setLicenseService(LicenseService licenseService) { XPackPlugin.licenseService.set(licenseService); }
     protected void setLicenseState(XPackLicenseState licenseState) { XPackPlugin.licenseState.set(licenseState); }
+    protected void setEpochMillisSupplier(LongSupplier epochMillisSupplier) {
+        XPackPlugin.epochMillisSupplier.set(epochMillisSupplier);
+    }
 
     public static SSLService getSharedSslService() {
         final SSLService ssl = XPackPlugin.sslService.get();
@@ -172,6 +178,7 @@ public class XPackPlugin extends XPackClientPlugin implements ExtensiblePlugin,
     }
     public static LicenseService getSharedLicenseService() { return licenseService.get(); }
     public static XPackLicenseState getSharedLicenseState() { return licenseState.get(); }
+    public static LongSupplier getSharedEpochMillisSupplier() { return epochMillisSupplier.get(); }
 
     /**
      * Checks if the cluster state allows this node to add x-pack metadata to the cluster state,
@@ -268,6 +275,8 @@ public class XPackPlugin extends XPackClientPlugin implements ExtensiblePlugin,
         setLicenseService(new LicenseService(settings, clusterService, getClock(),
                 environment, resourceWatcherService, getLicenseState()));
 
+        setEpochMillisSupplier(threadPool::absoluteTimeInMillis);
+
         // It is useful to override these as they are what guice is injecting into actions
         components.add(sslService);
         components.add(getLicenseService());
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java
index eb357661d50..01a3a421eba 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseFIPSTests.java
@@ -28,7 +28,7 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase {
             .put("xpack.security.transport.ssl.enabled", true)
             .put("xpack.security.fips_mode.enabled", randomBoolean())
             .build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
 
         setInitialState(null, licenseState, settings);
         licenseService.start();
@@ -52,7 +52,7 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase {
             .put("xpack.security.transport.ssl.enabled", true)
             .put("xpack.security.fips_mode.enabled", true)
             .build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
 
         setInitialState(null, licenseState, settings);
         licenseService.start();
@@ -67,7 +67,7 @@ public class LicenseFIPSTests extends AbstractLicenseServiceTestCase {
             .put("xpack.security.transport.ssl.enabled", true)
             .put("xpack.security.fips_mode.enabled", false)
             .build();
-        licenseState = new XPackLicenseState(settings);
+        licenseState = new XPackLicenseState(settings, () -> 0);
 
         setInitialState(null, licenseState, settings);
         licenseService.start();
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
index 754b398cd6c..2015461a2a7 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
@@ -33,7 +33,7 @@ public class LicenseTLSTests extends AbstractLicenseServiceTestCase {
         request.acknowledge(true);
         request.license(newLicense);
         Settings settings = Settings.builder().put("xpack.security.enabled", true).build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         inetAddress = InetAddress.getLoopbackAddress();
 
         setInitialState(null, licenseState, settings);
@@ -48,7 +48,7 @@ public class LicenseTLSTests extends AbstractLicenseServiceTestCase {
                 .put("discovery.type", "single-node")
                 .build();
         licenseService.stop();
-        licenseState = new XPackLicenseState(settings);
+        licenseState = new XPackLicenseState(settings, () -> 0);
         setInitialState(null, licenseState, settings);
         licenseService.start();
         licenseService.registerLicense(request, responseFuture);
@@ -62,7 +62,7 @@ public class LicenseTLSTests extends AbstractLicenseServiceTestCase {
         request.acknowledge(true);
         request.license(newLicense);
         Settings settings = Settings.builder().put("xpack.security.enabled", true).build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         inetAddress = TransportAddress.META_ADDRESS;
 
         setInitialState(null, licenseState, settings);
@@ -74,7 +74,7 @@ public class LicenseTLSTests extends AbstractLicenseServiceTestCase {
 
         settings = Settings.builder().put("xpack.security.enabled", false).build();
         licenseService.stop();
-        licenseState = new XPackLicenseState(settings);
+        licenseState = new XPackLicenseState(settings, () -> 0);
         setInitialState(null, licenseState, settings);
         licenseService.start();
         licenseService.registerLicense(request, responseFuture);
@@ -85,7 +85,7 @@ public class LicenseTLSTests extends AbstractLicenseServiceTestCase {
                 .put("xpack.security.transport.ssl.enabled", true)
                 .build();
         licenseService.stop();
-        licenseState = new XPackLicenseState(settings);
+        licenseState = new XPackLicenseState(settings, () -> 0);
         setInitialState(null, licenseState, settings);
         licenseService.start();
         licenseService.registerLicense(request, responseFuture);
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
index 02129a099b7..27222105f3e 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
@@ -362,7 +362,7 @@ public class TestUtils {
         public final List<Version> trialVersionUpdates = new ArrayList<>();
 
         public AssertingLicenseState() {
-            super(Settings.EMPTY);
+            super(Settings.EMPTY, () -> 0);
         }
 
         @Override
@@ -383,7 +383,7 @@ public class TestUtils {
         }
 
         public UpdatableLicenseState(Settings settings) {
-            super(settings);
+            super(settings, () -> 0);
         }
 
         @Override
@@ -393,7 +393,7 @@ public class TestUtils {
     }
 
     public static XPackLicenseState newTestLicenseState() {
-        return new XPackLicenseState(Settings.EMPTY);
+        return new XPackLicenseState(Settings.EMPTY, () -> 0);
     }
 
     public static void putLicense(Metadata.Builder builder, License license) {
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java
index 287f23e31e9..4aa2ebbb1e9 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java
@@ -15,6 +15,7 @@ import org.elasticsearch.xpack.core.XPackField;
 import org.elasticsearch.xpack.core.XPackSettings;
 
 import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -25,6 +26,9 @@ import static org.elasticsearch.license.License.OperationMode.PLATINUM;
 import static org.elasticsearch.license.License.OperationMode.STANDARD;
 import static org.elasticsearch.license.License.OperationMode.TRIAL;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.hamcrest.collection.IsMapContaining.hasKey;
+import static org.hamcrest.core.IsNot.not;
 
 /**
  * Unit tests for the {@link XPackLicenseState}
@@ -77,7 +81,7 @@ public class XPackLicenseStateTests extends ESTestCase {
 
     public void testSecurityDefaults() {
         Settings settings = Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         assertThat(licenseState.isSecurityEnabled(), is(true));
         assertThat(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING), is(true));
         assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true));
@@ -92,8 +96,7 @@ public class XPackLicenseStateTests extends ESTestCase {
 
     public void testTransportSslDoesNotAutomaticallyEnableSecurityOnTrialLicense() {
         Settings settings = Settings.builder().put(XPackSettings.TRANSPORT_SSL_ENABLED.getKey(), true).build();
-        final XPackLicenseState licenseState;
-        licenseState = new XPackLicenseState(settings);
+        final XPackLicenseState licenseState= new XPackLicenseState(settings, () -> 0);
         assertSecurityNotAllowed(licenseState);
     }
 
@@ -116,7 +119,7 @@ public class XPackLicenseStateTests extends ESTestCase {
 
     public void testSecurityBasicWithExplicitSecurityEnabled() {
         final Settings settings = Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(BASIC, true, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -148,7 +151,7 @@ public class XPackLicenseStateTests extends ESTestCase {
 
     public void testSecurityEnabledBasicExpired() {
         Settings settings = Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build();
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(BASIC, false, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -164,7 +167,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityStandard() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(STANDARD, true, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -178,7 +181,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityStandardExpired() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(STANDARD, false, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -192,7 +195,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityGold() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(GOLD, true, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -209,7 +212,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityGoldExpired() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(GOLD, false, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -226,7 +229,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityPlatinum() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(PLATINUM, true, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -243,7 +246,7 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testSecurityPlatinumExpired() {
         Settings settings = randomFrom(Settings.EMPTY,
             Settings.builder().put(XPackSettings.SECURITY_ENABLED.getKey(), true).build());
-        XPackLicenseState licenseState = new XPackLicenseState(settings);
+        XPackLicenseState licenseState = new XPackLicenseState(settings, () -> 0);
         licenseState.update(PLATINUM, false, null);
 
         assertThat(licenseState.isSecurityEnabled(), is(true));
@@ -566,4 +569,24 @@ public class XPackLicenseStateTests extends ESTestCase {
     public void testTransformInactiveBasic() {
         assertAllowed(BASIC, false, s -> s.checkFeature(Feature.TRANSFORM), false);
     }
+
+    public void testLastUsed() {
+        Feature basicFeature = Feature.SECURITY;
+        Feature goldFeature = Feature.SECURITY_DLS_FLS;
+        AtomicInteger currentTime = new AtomicInteger(100); // non zero start time
+        XPackLicenseState licenseState = new XPackLicenseState(Settings.EMPTY, currentTime::get);
+        assertThat("basic features not tracked", licenseState.getLastUsed(), not(hasKey(basicFeature)));
+        assertThat("initial epoch time", licenseState.getLastUsed(), not(hasKey(goldFeature)));
+        licenseState.isAllowed(basicFeature);
+        assertThat("basic features still not tracked", licenseState.getLastUsed(), not(hasKey(basicFeature)));
+        licenseState.isAllowed(goldFeature);
+        assertThat("isAllowed does not track", licenseState.getLastUsed(), not(hasKey(goldFeature)));
+        licenseState.checkFeature(basicFeature);
+        assertThat("basic features still not tracked", licenseState.getLastUsed(), not(hasKey(basicFeature)));
+        licenseState.checkFeature(goldFeature);
+        assertThat("checkFeature tracks used time", licenseState.getLastUsed(), hasEntry(goldFeature, 100L));
+        currentTime.set(200);
+        licenseState.checkFeature(goldFeature);
+        assertThat("checkFeature updates tracked time", licenseState.getLastUsed(), hasEntry(goldFeature, 200L));
+    }
 }
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java
index 083555ebcd5..a9066e7faca 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java
@@ -90,6 +90,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
+import java.util.function.LongSupplier;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
@@ -103,6 +104,7 @@ public class LocalStateCompositeXPackPlugin extends XPackPlugin implements Scrip
     private XPackLicenseState licenseState;
     private SSLService sslService;
     private LicenseService licenseService;
+    private LongSupplier epochMillisSupplier;
     protected List<Plugin> plugins = new ArrayList<>();
 
     public LocalStateCompositeXPackPlugin(final Settings settings, final Path configPath) {
@@ -150,6 +152,15 @@ public class LocalStateCompositeXPackPlugin extends XPackPlugin implements Scrip
         return modules;
     }
 
+    protected LongSupplier getEpochMillisSupplier() {
+        return epochMillisSupplier;
+    }
+
+    @Override
+    protected void setEpochMillisSupplier(LongSupplier epochMillisSupplier) {
+        this.epochMillisSupplier = epochMillisSupplier;
+    }
+
     @Override
     public Collection<Object> createComponents(Client client, ClusterService clusterService, ThreadPool threadPool,
                                                ResourceWatcherService resourceWatcherService, ScriptService scriptService,
diff --git a/x-pack/plugin/core/src/yamlRestTest/java/org/elasticsearch/license/XPackCoreClientYamlTestSuiteIT.java b/x-pack/plugin/core/src/yamlRestTest/java/org/elasticsearch/license/XPackCoreClientYamlTestSuiteIT.java
new file mode 100644
index 00000000000..dce47903125
--- /dev/null
+++ b/x-pack/plugin/core/src/yamlRestTest/java/org/elasticsearch/license/XPackCoreClientYamlTestSuiteIT.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.license;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
+import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
+
+import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
+
+public class XPackCoreClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
+
+    private static final String BASIC_AUTH_VALUE =
+        basicAuthHeaderValue("x_pack_rest_user", new SecureString("x-pack-test-password"));
+
+    public XPackCoreClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) {
+        super(testCandidate);
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() throws Exception {
+        return ESClientYamlSuiteTestCase.createParameters();
+    }
+
+    @Override
+    protected Settings restClientSettings() {
+        return Settings.builder()
+            .put(ThreadContext.PREFIX + ".Authorization", BASIC_AUTH_VALUE)
+            .build();
+    }
+}
diff --git a/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/api/license.get_feature_usage.json b/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/api/license.get_feature_usage.json
new file mode 100644
index 00000000000..57faa00c161
--- /dev/null
+++ b/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/api/license.get_feature_usage.json
@@ -0,0 +1,17 @@
+{
+  "license.get_feature_usage":{
+    "stability":"experimental",
+    "url":{
+      "paths":[
+        {
+          "path":"/_license/feature_usage",
+          "methods":[
+            "GET"
+          ]
+        }
+      ]
+    },
+    "params":{
+    }
+  }
+}
diff --git a/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/test/license/10_feature_usage.yml b/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/test/license/10_feature_usage.yml
new file mode 100644
index 00000000000..385bc57f1e8
--- /dev/null
+++ b/x-pack/plugin/core/src/yamlRestTest/resources/rest-api-spec/test/license/10_feature_usage.yml
@@ -0,0 +1,6 @@
+---
+"No features should be used just by starting up with default configuration":
+  - do:
+      license.get_feature_usage: {}
+
+  - length: { features: 0 }
diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/InvalidLicenseEnforcer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/InvalidLicenseEnforcer.java
index d89e3c75660..5be9a3c91dd 100644
--- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/InvalidLicenseEnforcer.java
+++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/InvalidLicenseEnforcer.java
@@ -48,7 +48,7 @@ public class InvalidLicenseEnforcer implements LicenseStateListener {
     @Override
     public void licenseStateChanged() {
         assert licenseStateListenerRegistered;
-        if (licenseState.checkFeature(XPackLicenseState.Feature.MACHINE_LEARNING) == false) {
+        if (licenseState.isAllowed(XPackLicenseState.Feature.MACHINE_LEARNING) == false) {
             // if the license has expired, close jobs and datafeeds
             threadPool.generic().execute(new AbstractRunnable() {
                 @Override
diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSetTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSetTests.java
index 2d1f99e32b7..98220753815 100644
--- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSetTests.java
+++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSetTests.java
@@ -338,7 +338,7 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
     }
 
     public void testUsageWithOrphanedTask() throws Exception {
-        when(licenseState.checkFeature(XPackLicenseState.Feature.MACHINE_LEARNING)).thenReturn(true);
+        when(licenseState.isAllowed(XPackLicenseState.Feature.MACHINE_LEARNING)).thenReturn(true);
         Settings.Builder settings = Settings.builder().put(commonSettings);
         settings.put("xpack.ml.enabled", true);
 
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
index dfe327aa984..36f1fbf21be 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
@@ -1057,11 +1057,14 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin,
         if (enabled) {
             return index -> {
                 XPackLicenseState licenseState = getLicenseState();
-                if (licenseState.isSecurityEnabled() == false || licenseState.checkFeature(Feature.SECURITY_DLS_FLS) == false) {
+                if (licenseState.isSecurityEnabled() == false) {
                     return MapperPlugin.NOOP_FIELD_PREDICATE;
                 }
                 IndicesAccessControl indicesAccessControl = threadContext.get().getTransient(
                         AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
+                if (indicesAccessControl == null) {
+                    return MapperPlugin.NOOP_FIELD_PREDICATE;
+                }
                 IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index);
                 if (indexPermissions == null) {
                     return MapperPlugin.NOOP_FIELD_PREDICATE;
@@ -1073,6 +1076,10 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin,
                 if (fieldPermissions.hasFieldLevelSecurity() == false) {
                     return MapperPlugin.NOOP_FIELD_PREDICATE;
                 }
+                if (licenseState.checkFeature(Feature.SECURITY_DLS_FLS) == false) {
+                    // check license last, once we know FLS is actually used
+                    return MapperPlugin.NOOP_FIELD_PREDICATE;
+                }
                 return fieldPermissions::grantsAccessTo;
             };
         }
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java
index c8c4f0c7516..6947407cf64 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java
@@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.bulk.BulkItemRequest;
 import org.elasticsearch.action.bulk.BulkShardRequest;
 import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.common.MemoizedSupplier;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState.Feature;
@@ -41,7 +42,8 @@ public class BulkShardRequestInterceptor implements RequestInterceptor {
     @Override
     public void intercept(RequestInfo requestInfo, AuthorizationEngine authzEngine, AuthorizationInfo authorizationInfo,
                           ActionListener<Void> listener) {
-        boolean shouldIntercept = licenseState.isSecurityEnabled() && licenseState.checkFeature(Feature.SECURITY_DLS_FLS);
+        boolean shouldIntercept = licenseState.isSecurityEnabled();
+        MemoizedSupplier<Boolean> licenseChecker = new MemoizedSupplier<>(() -> licenseState.checkFeature(Feature.SECURITY_DLS_FLS));
         if (requestInfo.getRequest() instanceof BulkShardRequest && shouldIntercept) {
             IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
 
@@ -54,7 +56,7 @@ public class BulkShardRequestInterceptor implements RequestInterceptor {
                     boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
                     boolean dls = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
                     if (fls || dls) {
-                        if (bulkItemRequest.request() instanceof UpdateRequest) {
+                        if (licenseChecker.get() && bulkItemRequest.request() instanceof UpdateRequest) {
                             found = true;
                             logger.trace("aborting bulk item update request for index [{}]", bulkItemRequest.index());
                             bulkItemRequest.abort(bulkItemRequest.index(), new ElasticsearchSecurityException("Can't execute a bulk " +
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java
index 600b11532dd..44d8eff03f8 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java
@@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.IndicesRequest;
+import org.elasticsearch.common.MemoizedSupplier;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState.Feature;
@@ -39,7 +40,8 @@ abstract class FieldAndDocumentLevelSecurityRequestInterceptor implements Reques
                           ActionListener<Void> listener) {
         if (requestInfo.getRequest() instanceof IndicesRequest) {
             IndicesRequest indicesRequest = (IndicesRequest) requestInfo.getRequest();
-            boolean shouldIntercept = licenseState.isSecurityEnabled() && licenseState.checkFeature(Feature.SECURITY_DLS_FLS);
+            boolean shouldIntercept = licenseState.isSecurityEnabled();
+            MemoizedSupplier<Boolean> licenseChecker = new MemoizedSupplier<>(() -> licenseState.checkFeature(Feature.SECURITY_DLS_FLS));
             if (supports(indicesRequest) && shouldIntercept) {
                 final IndicesAccessControl indicesAccessControl =
                     threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
@@ -48,7 +50,7 @@ abstract class FieldAndDocumentLevelSecurityRequestInterceptor implements Reques
                     if (indexAccessControl != null) {
                         boolean fieldLevelSecurityEnabled = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
                         boolean documentLevelSecurityEnabled = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
-                        if (fieldLevelSecurityEnabled || documentLevelSecurityEnabled) {
+                        if ((fieldLevelSecurityEnabled || documentLevelSecurityEnabled) && licenseChecker.get()) {
                             logger.trace("intercepted request for index [{}] with field level access controls [{}] " +
                                 "document level access controls [{}]. disabling conflicting features",
                                 index, fieldLevelSecurityEnabled, documentLevelSecurityEnabled);
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java
index b95e8410f81..0a2ae493554 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java
@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authz.interceptor;
 import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.common.MemoizedSupplier;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.license.XPackLicenseState;
@@ -52,23 +53,23 @@ public final class IndicesAliasesRequestInterceptor implements RequestIntercepto
             final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState();
             final AuditTrail auditTrail = auditTrailService.get();
             if (frozenLicenseState.isSecurityEnabled()) {
-                if (frozenLicenseState.checkFeature(Feature.SECURITY_DLS_FLS)) {
-                    IndicesAccessControl indicesAccessControl =
-                        threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
-                    for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) {
-                        if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) {
-                            for (String index : aliasAction.indices()) {
-                                IndicesAccessControl.IndexAccessControl indexAccessControl =
-                                    indicesAccessControl.getIndexPermissions(index);
-                                if (indexAccessControl != null) {
-                                    final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
-                                    final boolean dls = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
-                                    if (fls || dls) {
-                                        listener.onFailure(new ElasticsearchSecurityException("Alias requests are not allowed for " +
-                                            "users who have field or document level security enabled on one of the indices",
-                                            RestStatus.BAD_REQUEST));
-                                        return;
-                                    }
+                MemoizedSupplier<Boolean> licenseChecker =
+                    new MemoizedSupplier<>(() -> frozenLicenseState.checkFeature(Feature.SECURITY_DLS_FLS));
+                IndicesAccessControl indicesAccessControl =
+                    threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
+                for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) {
+                    if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) {
+                        for (String index : aliasAction.indices()) {
+                            IndicesAccessControl.IndexAccessControl indexAccessControl =
+                                indicesAccessControl.getIndexPermissions(index);
+                            if (indexAccessControl != null) {
+                                final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
+                                final boolean dls = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
+                                if ((fls || dls) && licenseChecker.get()) {
+                                    listener.onFailure(new ElasticsearchSecurityException("Alias requests are not allowed for " +
+                                        "users who have field or document level security enabled on one of the indices",
+                                        RestStatus.BAD_REQUEST));
+                                    return;
                                 }
                             }
                         }
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java
index 976acae3a29..4d9db4d4019 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java
@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authz.interceptor;
 import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
+import org.elasticsearch.common.MemoizedSupplier;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState.Feature;
@@ -48,19 +49,19 @@ public final class ResizeRequestInterceptor implements RequestInterceptor {
             final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState();
             final AuditTrail auditTrail = auditTrailService.get();
             if (frozenLicenseState.isSecurityEnabled()) {
-                if (frozenLicenseState.checkFeature(Feature.SECURITY_DLS_FLS)) {
-                    IndicesAccessControl indicesAccessControl =
-                        threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
-                    IndicesAccessControl.IndexAccessControl indexAccessControl =
-                        indicesAccessControl.getIndexPermissions(request.getSourceIndex());
-                    if (indexAccessControl != null) {
-                        final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
-                        final boolean dls = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
-                        if (fls || dls) {
-                            listener.onFailure(new ElasticsearchSecurityException("Resize requests are not allowed for users when " +
-                                "field or document level security is enabled on the source index", RestStatus.BAD_REQUEST));
-                            return;
-                        }
+                MemoizedSupplier<Boolean> licenseChecker =
+                    new MemoizedSupplier<>(() -> frozenLicenseState.checkFeature(Feature.SECURITY_DLS_FLS));
+                IndicesAccessControl indicesAccessControl =
+                    threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
+                IndicesAccessControl.IndexAccessControl indexAccessControl =
+                    indicesAccessControl.getIndexPermissions(request.getSourceIndex());
+                if (indexAccessControl != null) {
+                    final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
+                    final boolean dls = indexAccessControl.getDocumentPermissions().hasDocumentLevelPermissions();
+                    if ((fls || dls) && licenseChecker.get()) {
+                        listener.onFailure(new ElasticsearchSecurityException("Resize requests are not allowed for users when " +
+                            "field or document level security is enabled on the source index", RestStatus.BAD_REQUEST));
+                        return;
                     }
                 }
 
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java
index 46ae8f37dec..2e6c691f236 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java
@@ -168,12 +168,14 @@ public class CompositeRolesStore {
                                     rolesRetrievalResult.getMissingRoles()));
                         }
                         final Set<RoleDescriptor> effectiveDescriptors;
-                        if (licenseState.checkFeature(Feature.SECURITY_DLS_FLS)) {
-                            effectiveDescriptors = rolesRetrievalResult.getRoleDescriptors();
+                        Set<RoleDescriptor> roleDescriptors = rolesRetrievalResult.getRoleDescriptors();
+                        if (roleDescriptors.stream().anyMatch(RoleDescriptor::isUsingDocumentOrFieldLevelSecurity) &&
+                            licenseState.checkFeature(Feature.SECURITY_DLS_FLS) == false) {
+                            effectiveDescriptors = roleDescriptors.stream()
+                                .filter(r -> r.isUsingDocumentOrFieldLevelSecurity() == false)
+                                .collect(Collectors.toSet());
                         } else {
-                            effectiveDescriptors = rolesRetrievalResult.getRoleDescriptors().stream()
-                                    .filter((rd) -> rd.isUsingDocumentOrFieldLevelSecurity() == false)
-                                    .collect(Collectors.toSet());
+                            effectiveDescriptors = roleDescriptors;
                         }
                         logger.trace(() -> new ParameterizedMessage("Exposing effective role descriptors [{}] for role names [{}]",
                                 effectiveDescriptors, roleNames));
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java
index 7473a0a2781..88410d858ec 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java
@@ -12,6 +12,7 @@ import org.apache.logging.log4j.util.Supplier;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.common.MemoizedSupplier;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.set.Sets;
@@ -176,14 +177,15 @@ public class FileRolesStore implements BiConsumer<Set<String>, ActionListener<Ro
         if (Files.exists(path)) {
             try {
                 List<String> roleSegments = roleSegments(path);
-                final boolean flsDlsLicensed = licenseState.checkFeature(Feature.SECURITY_DLS_FLS);
+                MemoizedSupplier<Boolean> licenseChecker =
+                    new MemoizedSupplier<>(() -> licenseState.checkFeature(Feature.SECURITY_DLS_FLS));
                 for (String segment : roleSegments) {
                     RoleDescriptor descriptor = parseRoleDescriptor(segment, path, logger, resolvePermission, settings, xContentRegistry);
                     if (descriptor != null) {
                         if (ReservedRolesStore.isReserved(descriptor.getName())) {
                             logger.warn("role [{}] is reserved. the relevant role definition in the mapping file will be ignored",
                                     descriptor.getName());
-                        } else if (flsDlsLicensed == false && descriptor.isUsingDocumentOrFieldLevelSecurity()) {
+                        } else if (descriptor.isUsingDocumentOrFieldLevelSecurity() && licenseChecker.get() == false) {
                             logger.warn("role [{}] uses document and/or field level security, which is not enabled by the current license" +
                                     ". this role will be ignored", descriptor.getName());
                             // we still put the role in the map to avoid unnecessary negative lookups
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java
index b471b66cfaa..ed531fe1b8c 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java
@@ -201,9 +201,7 @@ public class NativeRolesStore implements BiConsumer<Set<String>, ActionListener<
     }
 
     public void putRole(final PutRoleRequest request, final RoleDescriptor role, final ActionListener<Boolean> listener) {
-        if (licenseState.checkFeature(Feature.SECURITY_DLS_FLS)) {
-            innerPutRole(request, role, listener);
-        } else if (role.isUsingDocumentOrFieldLevelSecurity()) {
+        if (role.isUsingDocumentOrFieldLevelSecurity() && licenseState.checkFeature(Feature.SECURITY_DLS_FLS) == false) {
             listener.onFailure(LicenseUtils.newComplianceException("field and document level security"));
         } else {
             innerPutRole(request, role, listener);
@@ -382,30 +380,25 @@ public class NativeRolesStore implements BiConsumer<Set<String>, ActionListener<
             // we pass true as last parameter because we do not want to reject permissions if the field permissions
             // are given in 2.x syntax
             RoleDescriptor roleDescriptor = RoleDescriptor.parse(name, sourceBytes, true, XContentType.JSON);
-            if (licenseState.checkFeature(Feature.SECURITY_DLS_FLS)) {
-                return roleDescriptor;
-            } else {
-                final boolean dlsEnabled =
-                        Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingDocumentLevelSecurity);
-                final boolean flsEnabled =
-                        Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity);
-                if (dlsEnabled || flsEnabled) {
-                    List<String> unlicensedFeatures = new ArrayList<>(2);
-                    if (flsEnabled) {
-                        unlicensedFeatures.add("fls");
-                    }
-                    if (dlsEnabled) {
-                        unlicensedFeatures.add("dls");
-                    }
-                    Map<String, Object> transientMap = new HashMap<>(2);
-                    transientMap.put("unlicensed_features", unlicensedFeatures);
-                    transientMap.put("enabled", false);
-                    return new RoleDescriptor(roleDescriptor.getName(), roleDescriptor.getClusterPrivileges(),
-                            roleDescriptor.getIndicesPrivileges(), roleDescriptor.getRunAs(), roleDescriptor.getMetadata(), transientMap);
-                } else {
-                    return roleDescriptor;
+            final boolean dlsEnabled =
+                    Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingDocumentLevelSecurity);
+            final boolean flsEnabled =
+                    Arrays.stream(roleDescriptor.getIndicesPrivileges()).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity);
+            if ((dlsEnabled || flsEnabled) && licenseState.checkFeature(Feature.SECURITY_DLS_FLS) == false) {
+                List<String> unlicensedFeatures = new ArrayList<>(2);
+                if (flsEnabled) {
+                    unlicensedFeatures.add("fls");
                 }
-
+                if (dlsEnabled) {
+                    unlicensedFeatures.add("dls");
+                }
+                Map<String, Object> transientMap = new HashMap<>(2);
+                transientMap.put("unlicensed_features", unlicensedFeatures);
+                transientMap.put("enabled", false);
+                return new RoleDescriptor(roleDescriptor.getName(), roleDescriptor.getClusterPrivileges(),
+                        roleDescriptor.getIndicesPrivileges(), roleDescriptor.getRunAs(), roleDescriptor.getMetadata(), transientMap);
+            } else {
+                return roleDescriptor;
             }
         } catch (Exception e) {
             logger.error(new ParameterizedMessage("error in the format of data for role [{}]", name), e);
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java
index 41c98049775..6c0b19b1126 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java
@@ -90,7 +90,7 @@ public class LoggingAuditTrailFilterTests extends ESTestCase {
             arg0.updateLocalNodeInfo(localNode);
             return null;
         }).when(clusterService).addListener(Mockito.isA(LoggingAuditTrail.class));
-        apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), mock(Client.class), new XPackLicenseState(settings),
+        apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), mock(Client.class), new XPackLicenseState(settings, () -> 0),
                 mock(SecurityIndexManager.class), clusterService, mock(ThreadPool.class));
     }
 
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java
index 78060b1e044..2df38c07c41 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java
@@ -220,7 +220,7 @@ public class LoggingAuditTrailTests extends ESTestCase {
         }
         logger = CapturingLogger.newCapturingLogger(randomFrom(Level.OFF, Level.FATAL, Level.ERROR, Level.WARN, Level.INFO), patternLayout);
         auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext);
-        apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, new XPackLicenseState(settings),
+        apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, new XPackLicenseState(settings, () -> 0),
                 securityIndexManager, clusterService, mock(ThreadPool.class));
     }
 
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
index e6eb2e511c8..a9344f107a6 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java
@@ -689,7 +689,7 @@ public class AuthorizationServiceTests extends ESTestCase {
         final AnonymousUser anonymousUser = new AnonymousUser(settings);
         authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService,
             new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(),
-            new XPackLicenseState(settings), new IndexNameExpressionResolver());
+            new XPackLicenseState(settings, () -> 0), new IndexNameExpressionResolver());
 
         RoleDescriptor role = new RoleDescriptor("a_all", null,
             new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null);
@@ -717,7 +717,7 @@ public class AuthorizationServiceTests extends ESTestCase {
         final Authentication authentication = createAuthentication(new AnonymousUser(settings));
         authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrailService,
             new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null,
-            Collections.emptySet(), new XPackLicenseState(settings), new IndexNameExpressionResolver());
+            Collections.emptySet(), new XPackLicenseState(settings, () -> 0), new IndexNameExpressionResolver());
 
         RoleDescriptor role = new RoleDescriptor("a_all", null,
             new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null);
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java
index 6222d58203a..103cf46c42d 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java
@@ -354,7 +354,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache();
         final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore,
             reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(settings),
-            new XPackLicenseState(settings), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
+            new XPackLicenseState(settings, () -> 0), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
             rds -> effectiveRoleDescriptors.set(rds));
         verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
 
@@ -393,7 +393,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
                 rds -> effectiveRoleDescriptors.set(rds));
         verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
 
@@ -480,7 +480,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
                 new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                                 mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, inMemoryProvider2),
-                                new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS),
+                                new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0),
                                 cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
                                 rds -> effectiveRoleDescriptors.set(rds));
 
@@ -709,7 +709,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, failingProvider),
-                new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS),
+                new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0),
                 cache, mock(ApiKeyService.class), documentSubsetBitsetCache, rds -> effectiveRoleDescriptors.set(rds));
 
         final Set<String> roleNames = Sets.newHashSet("roleA", "roleB", "unknown");
@@ -821,7 +821,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         CompositeRolesStore compositeRolesStore = new CompositeRolesStore(
                 Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
                 rds -> {}) {
             @Override
             public void invalidateAll() {
@@ -875,7 +875,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS,
                 fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class),
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, mock(ApiKeyService.class),
                 documentSubsetBitsetCache, rds -> {}) {
             @Override
             public void invalidateAll() {
@@ -971,7 +971,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
                 rds -> effectiveRoleDescriptors.set(rds));
         verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
 
@@ -1012,7 +1012,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, mock(ApiKeyService.class), documentSubsetBitsetCache,
                 rds -> effectiveRoleDescriptors.set(rds));
         verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
         IllegalArgumentException iae = expectThrows(IllegalArgumentException.class,
@@ -1035,7 +1035,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
         ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS);
         ApiKeyService apiKeyService = spy(new ApiKeyService(SECURITY_ENABLED_SETTINGS, Clock.systemUTC(), mock(Client.class),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), mock(SecurityIndexManager.class), mock(ClusterService.class),
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), mock(SecurityIndexManager.class), mock(ClusterService.class),
                 mock(ThreadPool.class)));
         NativePrivilegeStore nativePrivStore = mock(NativePrivilegeStore.class);
         doAnswer(invocationOnMock -> {
@@ -1050,7 +1050,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, apiKeyService, documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, apiKeyService, documentSubsetBitsetCache,
                 rds -> effectiveRoleDescriptors.set(rds));
         AuditUtil.getOrGenerateRequestId(threadContext);
         final Version version = randomFrom(Version.CURRENT, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1));
@@ -1088,7 +1088,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS);
 
         ApiKeyService apiKeyService = spy(new ApiKeyService(SECURITY_ENABLED_SETTINGS, Clock.systemUTC(), mock(Client.class),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), mock(SecurityIndexManager.class), mock(ClusterService.class),
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), mock(SecurityIndexManager.class), mock(ClusterService.class),
                 mock(ThreadPool.class)));
         NativePrivilegeStore nativePrivStore = mock(NativePrivilegeStore.class);
         doAnswer(invocationOnMock -> {
@@ -1103,7 +1103,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
         final CompositeRolesStore compositeRolesStore =
             new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
                 nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
-                new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, apiKeyService, documentSubsetBitsetCache,
+                new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0), cache, apiKeyService, documentSubsetBitsetCache,
                 rds -> effectiveRoleDescriptors.set(rds));
         AuditUtil.getOrGenerateRequestId(threadContext);
         final Version version = randomFrom(Version.CURRENT, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1));
@@ -1238,7 +1238,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
             nativePrivStore,
             Collections.emptyList(),
             new ThreadContext(SECURITY_ENABLED_SETTINGS),
-            new XPackLicenseState(SECURITY_ENABLED_SETTINGS),
+            new XPackLicenseState(SECURITY_ENABLED_SETTINGS, () -> 0),
             cache,
             apiKeyService,
             documentSubsetBitsetCache,
@@ -1364,7 +1364,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
             }).when(privilegeStore).getPrivileges(isA(Set.class), isA(Set.class), any(ActionListener.class));
         }
         if (licenseState == null) {
-            licenseState = new XPackLicenseState(settings);
+            licenseState = new XPackLicenseState(settings, () -> 0);
         }
         if (apiKeyService == null) {
             apiKeyService = mock(ApiKeyService.class);
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java
index d54495a23fb..23b73dc7155 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java
@@ -215,13 +215,13 @@ public class ServerTransportFilterTests extends ESTestCase {
         Settings settings = Settings.builder().put("path.home", createTempDir()).build();
         ThreadContext threadContext = new ThreadContext(settings);
         return new ServerTransportFilter.ClientProfile(authcService, authzService, threadContext, false, destructiveOperations,
-                reservedRealmEnabled, new SecurityContext(settings, threadContext), new XPackLicenseState(settings));
+                reservedRealmEnabled, new SecurityContext(settings, threadContext), new XPackLicenseState(settings, () -> 0));
     }
 
     private ServerTransportFilter.NodeProfile getNodeFilter(boolean reservedRealmEnabled) throws IOException {
         Settings settings = Settings.builder().put("path.home", createTempDir()).build();
         ThreadContext threadContext = new ThreadContext(settings);
         return new ServerTransportFilter.NodeProfile(authcService, authzService, threadContext, false, destructiveOperations,
-                reservedRealmEnabled, new SecurityContext(settings, threadContext), new XPackLicenseState(settings));
+                reservedRealmEnabled, new SecurityContext(settings, threadContext), new XPackLicenseState(settings, () -> 0));
     }
 }