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 index ebec0b088185f..7b8e86d202e51 100644 --- 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 @@ -7,12 +7,14 @@ package org.elasticsearch.license; +import org.elasticsearch.Version; 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 org.elasticsearch.core.Nullable; import java.io.IOException; import java.time.Instant; @@ -20,23 +22,31 @@ import java.time.ZonedDateTime; import java.util.Collections; import java.util.List; +import java.util.Objects; public class GetFeatureUsageResponse extends ActionResponse implements ToXContentObject { public static class FeatureUsageInfo implements Writeable { - public final String name; - public final ZonedDateTime lastUsedTime; + private final String name; + private final ZonedDateTime lastUsedTime; + private final String context; public final String licenseLevel; - public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, String licenseLevel) { - this.name = name; - this.lastUsedTime = lastUsedTime; - this.licenseLevel = licenseLevel; + public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, @Nullable String context, String licenseLevel) { + this.name = Objects.requireNonNull(name, "Feature name may not be null"); + this.lastUsedTime = Objects.requireNonNull(lastUsedTime, "Last used time may not be null"); + this.context = context; + this.licenseLevel = Objects.requireNonNull(licenseLevel, "License level may not be null"); } public FeatureUsageInfo(StreamInput in) throws IOException { this.name = in.readString(); this.lastUsedTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(in.readLong()), ZoneOffset.UTC); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.context = in.readOptionalString(); + } else { + this.context = null; + } this.licenseLevel = in.readString(); } @@ -44,6 +54,9 @@ public FeatureUsageInfo(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { out.writeString(name); out.writeLong(lastUsedTime.toEpochSecond()); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeOptionalString(this.context); + } out.writeString(licenseLevel); } } @@ -74,6 +87,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws for (FeatureUsageInfo feature : features) { builder.startObject(); builder.field("name", feature.name); + builder.field("context", feature.context); builder.field("last_used", feature.lastUsedTime.toString()); builder.field("license_level", feature.licenseLevel); builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java new file mode 100644 index 0000000000000..04c16179b3f08 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.license; + +import java.util.Objects; + +/** + * A base class for checking licensed features against the license. + */ +public abstract class LicensedFeature { + + public static class Momentary extends LicensedFeature { + + private Momentary(String name, License.OperationMode minimumOperationMode, boolean needsActive) { + super(name, minimumOperationMode, needsActive); + } + + /** + * Checks whether the feature is allowed by the given license state, and + * updates the last time the feature was used. + */ + public boolean check(XPackLicenseState state) { + if (state.isAllowed(this)) { + state.featureUsed(this); + return true; + } else { + return false; + } + } + } + + public static class Persistent extends LicensedFeature { + private Persistent(String name, License.OperationMode minimumOperationMode, boolean needsActive) { + super(name, minimumOperationMode, needsActive); + } + + public boolean checkAndStartTracking(XPackLicenseState state, String contextName) { + if (state.isAllowed(this)) { + state.enableUsageTracking(this, contextName); + return true; + } else { + return false; + } + } + + public void stopTracking(XPackLicenseState state, String contextName) { + state.disableUsageTracking(this, contextName); + } + } + + public static class Untracked extends LicensedFeature { + private Untracked(String name, License.OperationMode minimumOperationMode, boolean needsActive) { + super(name, minimumOperationMode, needsActive); + } + + public boolean check(XPackLicenseState state) { + return state.isAllowed(this); + } + } + + final String name; + final License.OperationMode minimumOperationMode; + final boolean needsActive; + + public LicensedFeature(String name, License.OperationMode minimumOperationMode, boolean needsActive) { + this.name = name; + this.minimumOperationMode = minimumOperationMode; + this.needsActive = needsActive; + } + + /** + * Creates a feature that is tracked at the moment it is checked. + * @param name A unique name for the feature that will be returned in + * the tracking API. This should not change. + * @param licenseLevel The lowest level of license in which this feature should be allowed. + */ + public static Momentary momentary(String name, License.OperationMode licenseLevel) { + return new Momentary(name, licenseLevel, true); + } + + public static Persistent persistent(String name, License.OperationMode licenseLevel) { + return new Persistent(name, licenseLevel, true); + } + + /** + * Creates a feature that is tracked at the moment it is checked, but that is lenient as + * to whether the license needs to be active to allow the feature. + */ + @Deprecated + public static Momentary momentaryLenient(String name, License.OperationMode licenseLevel) { + return new Momentary(name, licenseLevel, false); + } + + @Deprecated + public static Persistent persistentLenient(String name, License.OperationMode licenseLevel) { + return new Persistent(name, licenseLevel, false); + } + + /** + * Returns whether the feature is allowed by the current license + * without affecting feature tracking. + */ + public final boolean checkWithoutTracking(XPackLicenseState state) { + return state.isAllowed(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LicensedFeature that = (LicensedFeature) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} 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 index 32dee3b1e6923..46d36b7a22962 100644 --- 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 @@ -20,7 +20,6 @@ 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 { @@ -40,15 +39,19 @@ public TransportGetFeatureUsageAction(TransportService transportService, ActionF @Override protected void doExecute(Task task, GetFeatureUsageRequest request, ActionListener listener) { - Map featureUsage = licenseState.getLastUsed(); - List usageInfos = new ArrayList<>(); - for (var 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)); - } + Map featureUsage = licenseState.getLastUsed(); + List usageInfos = new ArrayList<>(featureUsage.size()); + featureUsage.forEach((usage, lastUsed) -> { + ZonedDateTime lastUsedTime = Instant.ofEpochMilli(lastUsed).atZone(ZoneOffset.UTC); + usageInfos.add( + new GetFeatureUsageResponse.FeatureUsageInfo( + usage.featureName(), + lastUsedTime, + usage.contextName(), + usage.minimumOperationMode().description() + ) + ); + }); 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 0a613f2be3293..1214c232eac20 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 @@ -7,25 +7,26 @@ package org.elasticsearch.license; import org.elasticsearch.Version; -import org.elasticsearch.core.Nullable; import org.elasticsearch.common.Strings; -import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Nullable; import org.elasticsearch.license.License.OperationMode; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.monitoring.MonitoringField; import java.util.Collections; -import java.util.EnumMap; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.LongAccumulator; @@ -33,7 +34,6 @@ import java.util.function.Function; import java.util.function.LongSupplier; import java.util.function.Predicate; -import java.util.stream.Collectors; import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD; @@ -89,17 +89,22 @@ public enum Feature { AUTOSCALING(OperationMode.ENTERPRISE, true); - final OperationMode minimumOperationMode; - final boolean needsActive; + // NOTE: this is temporary. The Feature enum will go away in favor of LicensedFeature. + // Embedding the feature instance here is a stopgap to allow smaller initial PR, + // followed by PRs to convert the current consumers of the license state. + final LicensedFeature.Momentary feature; Feature(OperationMode minimumOperationMode, boolean needsActive) { assert minimumOperationMode.compareTo(OperationMode.BASIC) > 0: minimumOperationMode.toString(); - this.minimumOperationMode = minimumOperationMode; - this.needsActive = needsActive; + if (needsActive) { + this.feature = LicensedFeature.momentary(name().toLowerCase(Locale.ROOT), minimumOperationMode); + } else { + this.feature = LicensedFeature.momentaryLenient(name().toLowerCase(Locale.ROOT), minimumOperationMode); + } } } - // temporarily non tracked feeatures which need rework in how they are checked + // temporarily non tracked features which need rework in how they are checked // so they are not tracked as always used private static final Set NON_TRACKED_FEATURES = Set.of( Feature.SECURITY_IP_FILTERING, @@ -403,7 +408,23 @@ private static class Status { private final List listeners; private final boolean isSecurityEnabled; private final boolean isSecurityExplicitlyEnabled; - private final Map lastUsed; + + /** + * A Map of features for which usage is tracked exclusive by last-used-time. + * This is the most common way feature usage is tracked, so these are handled as a special case to optimize for throughput + * (using a pre-populated map with LongAccumulator rather than concurrent map) + */ + private final Map featureLastUsed; + + /** + * A Map of features for which usage is tracked exclusive by a contextual identifier and, optionally, a last-used-time. + * A last used time of {@code -1} means that the feature is "on" and should report the current time as the last-used-time + * (See: {@link #epochMillisProvider}, {@link #getLastUsed}) + * The are a less common way of tracking feature usage, and cannot be known in advance (as the context is variable) so these must + * be managed by applying concurrent controls to the map. + */ + private final Map contextLastUsed; + private final LongSupplier epochMillisProvider; // Since Status is the only field that can be updated, we do not need to synchronize access to @@ -419,23 +440,35 @@ public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) { // prepopulate feature last used map with entries for non basic features, which are the ones we // care to actually keep track of - Map lastUsed = new EnumMap<>(Feature.class); + // TODO: Do we want this class to know about all features? Ideally not. In which case we will need to move to lazily creating + // the accumulators on first usage (or perhaps when the feature is instantiated). We can probably do that within a coase grained + // lock (synchronized?) since it will happen only once. + Map lastUsed = new HashMap<>(); for (Feature feature : Feature.values()) { if (NON_TRACKED_FEATURES.contains(feature) == false) { - lastUsed.put(feature, new LongAccumulator(Long::max, 0)); + lastUsed.put(feature.feature, new LongAccumulator(Long::max, 0)); } } - this.lastUsed = lastUsed; + this.featureLastUsed = lastUsed; + this.contextLastUsed = new ConcurrentHashMap<>(); // TODO or CopyOnWriteHashMap since writes are infrequent? this.epochMillisProvider = epochMillisProvider; } - private XPackLicenseState(List listeners, boolean isSecurityEnabled, boolean isSecurityExplicitlyEnabled, - Status status, Map lastUsed, LongSupplier epochMillisProvider) { + private XPackLicenseState( + List listeners, + boolean isSecurityEnabled, + boolean isSecurityExplicitlyEnabled, + Status status, + Map featureLastUsed, + Map contextLastUsed, + LongSupplier epochMillisProvider + ) { this.listeners = listeners; this.isSecurityEnabled = isSecurityEnabled; this.isSecurityExplicitlyEnabled = isSecurityExplicitlyEnabled; this.status = status; - this.lastUsed = lastUsed; + this.featureLastUsed = featureLastUsed; + this.contextLastUsed = contextLastUsed; this.epochMillisProvider = epochMillisProvider; } @@ -463,7 +496,7 @@ private boolean checkAgainstStatus(Predicate statusPredicate) { * May be {@code null} if they have never generated a trial license on this cluster, or the most recent * trial was prior to this metadata being tracked (6.1) */ - void update(OperationMode mode, boolean active, long expirationDate, @Nullable Version mostRecentTrialVersion) { + protected void update(OperationMode mode, boolean active, long expirationDate, @Nullable Version mostRecentTrialVersion) { status = new Status(mode, active, expirationDate); listeners.forEach(LicenseStateListener::licenseStateChanged); } @@ -483,33 +516,54 @@ public OperationMode getOperationMode() { return executeAgainstStatus(status -> status.mode); } - /** - * Checks that the cluster has a valid licence of any level. - * @see #isActive() - */ - public boolean allowForAllLicenses() { - return checkAgainstStatus(status -> status.active); - } - // Package private for tests /** Return true if the license is currently within its time boundaries, false otherwise. */ public boolean isActive() { return checkAgainstStatus(status -> status.active); } + @Deprecated + public boolean checkFeature(Feature feature) { + return feature.feature.check(this); + } + + void featureUsed(LicensedFeature feature) { + Optional.ofNullable(this.featureLastUsed.get(feature)).ifPresent(a -> a.accumulate(epochMillisProvider.getAsLong())); + checkForExpiry(feature); + } + + void enableUsageTracking(LicensedFeature feature, String contextName) { + FeatureUsage usage = new FeatureUsage(feature, Objects.requireNonNull(contextName, "Context name cannot be null")); + this.contextLastUsed.put(usage, -1L); + checkForExpiry(feature); + } + + void disableUsageTracking(LicensedFeature feature, String contextName) { + FeatureUsage usage = new FeatureUsage(feature, Objects.requireNonNull(contextName, "Context name cannot be null")); + this.contextLastUsed.replace(usage, -1L, epochMillisProvider.getAsLong()); + } + /** - * Checks whether the given feature is allowed, tracking the last usage time. + * Checks whether the given feature is allowed by the current license. + *

+ * This method should only be used when serializing whether a feature is allowed for telemetry. */ - @SuppressForbidden(reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE") - public boolean checkFeature(Feature feature) { - boolean allowed = isAllowed(feature); - LongAccumulator maxEpochAccumulator = lastUsed.get(feature); - final long licenseExpiryDate = getLicenseExpiryDate(); - final long diff = licenseExpiryDate - System.currentTimeMillis(); - if (maxEpochAccumulator != null) { - maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong()); + @Deprecated + public boolean isAllowed(Feature feature) { + return isAllowed(feature.feature); + } + + // Package protected: Only allowed to be called by LicensedFeature + boolean isAllowed(LicensedFeature feature) { + if (isAllowedByLicense(feature.minimumOperationMode, feature.needsActive)) { + return true; } + return false; + } + private void checkForExpiry(LicensedFeature feature) { + final long licenseExpiryDate = getLicenseExpiryDate(); + final long diff = licenseExpiryDate - System.currentTimeMillis(); if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 && LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) { final long days = TimeUnit.MILLISECONDS.toDays(diff); @@ -519,17 +573,6 @@ public boolean checkFeature(Feature feature) { HeaderWarning.addWarning("Your license {}. " + "Contact your administrator or update your license for continued use of features", expiryMessage); } - - return allowed; - } - - /** - * Checks whether the given feature is allowed by the current license. - *

- * This method should only be used when serializing whether a feature is allowed for telemetry. - */ - public boolean isAllowed(Feature feature) { - return isAllowedByLicense(feature.minimumOperationMode, feature.needsActive); } /** @@ -537,10 +580,22 @@ public boolean isAllowed(Feature feature) { * * Note that if a feature has not been used, it will not appear in the map. */ - public Map 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 Map getLastUsed() { + Map lastUsed = new HashMap<>(this.featureLastUsed.size() + this.contextLastUsed.size()); + featureLastUsed.forEach((feature, accumulator) -> { + long time = accumulator.get(); + if (time > 0) { + lastUsed.put(new FeatureUsage(feature, null), time); + } + }); + contextLastUsed.forEach((usage, time) -> { + if (time == -1) { + time = epochMillisProvider.getAsLong(); + lastUsed.put(usage, time); + } + }); + + return lastUsed; } public static boolean isMachineLearningAllowedForOperationMode(final OperationMode operationMode) { @@ -610,8 +665,17 @@ public static boolean isAllowedByOperationMode( * is needed for multiple interactions with the license state. */ public XPackLicenseState copyCurrentLicenseState() { - return executeAgainstStatus(status -> - new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider)); + return executeAgainstStatus( + status -> new XPackLicenseState( + listeners, + isSecurityEnabled, + isSecurityExplicitlyEnabled, + status, + featureLastUsed, + contextLastUsed, + epochMillisProvider + ) + ); } /** @@ -622,6 +686,7 @@ public XPackLicenseState copyCurrentLicenseState() { * * @return true if feature is allowed, otherwise false */ + @Deprecated public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive) { return checkAgainstStatus(status -> { if (needActive && false == status.active) { @@ -646,4 +711,45 @@ public boolean isAllowedByLicense(OperationMode minimumMode) { return isAllowedByLicense(minimumMode, true); } + public static class FeatureUsage { + private final LicensedFeature feature; + + @Nullable + private final String context; + + private FeatureUsage(LicensedFeature feature, String context) { + this.feature = Objects.requireNonNull(feature, "Feature cannot be null"); + this.context = context; + } + + @Override + public String toString() { + return context == null ? feature.name : feature.name + ":" + context; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureUsage usage = (FeatureUsage) o; + return Objects.equals(feature, usage.feature) && Objects.equals(context, usage.context); + } + + @Override + public int hashCode() { + return Objects.hash(feature, context); + } + + public String featureName() { + return feature.name; + } + + public String contextName() { + return context; + } + + public OperationMode minimumOperationMode() { + return feature.minimumOperationMode; + } + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java new file mode 100644 index 0000000000000..dcf5c652e7081 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.license; + +import org.elasticsearch.common.settings.Settings; + +import java.util.function.LongSupplier; + +/** A license state that may be mocked by testing because the internal methods are made public */ +public class MockLicenseState extends XPackLicenseState { + + public MockLicenseState(Settings settings, LongSupplier epochMillisProvider) { + super(settings, epochMillisProvider); + } + + @Override + public boolean isAllowed(LicensedFeature feature) { + return super.isAllowed(feature); + } + + @Override + public void enableUsageTracking(LicensedFeature feature, String contextName) { + super.enableUsageTracking(feature, contextName); + } +} 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 9376d1f63c64e..45b3ddff6270e 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 @@ -46,6 +46,9 @@ import static org.elasticsearch.test.ESTestCase.randomIntBetween; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class TestUtils { @@ -368,7 +371,7 @@ public AssertingLicenseState() { } @Override - void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) { + protected void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) { modeUpdates.add(mode); activeUpdates.add(active); expirationDateUpdates.add(expirationDate); @@ -402,4 +405,14 @@ public static XPackLicenseState newTestLicenseState() { public static void putLicense(Metadata.Builder builder, License license) { builder.putCustom(LicensesMetadata.TYPE, new LicensesMetadata(license, null)); } + + public static MockLicenseState newMockLicenceState() { + MockLicenseState mock = mock(MockLicenseState.class); + // These are deprecated methods, but we haven't replaced all usage of them yet + // By calling the real methods, we force everything through a small number of mockable methods like + // XPackLicenseState.isAllowed(LicensedFeature) + when(mock.isAllowed(any(XPackLicenseState.Feature.class))).thenCallRealMethod(); + when(mock.checkFeature(any(XPackLicenseState.Feature.class))).thenCallRealMethod(); + return mock; + } } 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 b8a3ba24c8345..bcf4b78cd5754 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 @@ -8,6 +8,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.iterable.Iterables; import org.elasticsearch.license.License.OperationMode; import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.test.ESTestCase; @@ -16,6 +17,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import java.util.Arrays; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -27,8 +29,11 @@ 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.aMapWithSize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.collection.IsMapContaining.hasKey; import static org.hamcrest.core.IsNot.not; @@ -513,16 +518,29 @@ public void testCcrAckTrialOrPlatinumToNotTrialOrPlatinum() { } public void testLastUsed() { - Feature goldFeature = Feature.SECURITY_DLS_FLS; + LicensedFeature.Momentary goldFeature = Feature.SECURITY_DLS_FLS.feature; AtomicInteger currentTime = new AtomicInteger(100); // non zero start time XPackLicenseState licenseState = new XPackLicenseState(Settings.EMPTY, currentTime::get); - assertThat("initial epoch time", licenseState.getLastUsed(), not(hasKey(goldFeature))); + Map lastUsed = licenseState.getLastUsed(); + assertThat("initial epoch time", lastUsed, not(hasKey(goldFeature))); + licenseState.isAllowed(goldFeature); - assertThat("isAllowed does not track", licenseState.getLastUsed(), not(hasKey(goldFeature))); - licenseState.checkFeature(goldFeature); - assertThat("checkFeature tracks used time", licenseState.getLastUsed(), hasEntry(goldFeature, 100L)); + lastUsed = licenseState.getLastUsed(); + assertThat("isAllowed does not track", lastUsed, not(hasKey(goldFeature))); + + goldFeature.check(licenseState); + lastUsed = licenseState.getLastUsed(); + assertThat("feature.check tracks usage", lastUsed, aMapWithSize(1)); + + XPackLicenseState.FeatureUsage usage = Iterables.get(licenseState.getLastUsed().keySet(), 0); + assertThat(usage.featureName(), equalTo(Feature.SECURITY_DLS_FLS.feature.name)); + assertThat(usage.contextName(), nullValue()); + assertThat(lastUsed.get(usage), equalTo(100L)); + currentTime.set(200); - licenseState.checkFeature(goldFeature); - assertThat("checkFeature updates tracked time", licenseState.getLastUsed(), hasEntry(goldFeature, 200L)); + goldFeature.check(licenseState); + lastUsed = licenseState.getLastUsed(); + assertThat("feature.check updates usage", lastUsed.keySet(), containsInAnyOrder(usage)); + assertThat(lastUsed.get(usage), equalTo(200L)); } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/license/LicensingTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/license/LicensingTests.java index 8fd24370ad412..30dce4994fe0c 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/license/LicensingTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/license/LicensingTests.java @@ -37,6 +37,7 @@ import org.elasticsearch.transport.Netty4Plugin; import org.elasticsearch.transport.TransportInfo; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.LocalStateSecurity; import org.hamcrest.Matchers; import org.junit.After; @@ -63,6 +64,9 @@ import static org.hamcrest.Matchers.notNullValue; public class LicensingTests extends SecurityIntegTestCase { + + private static final SecureString HASH_PASSWD = new SecureString(Hasher.BCRYPT4.hash(new SecureString("passwd".toCharArray()))); + private static final String ROLES = SecuritySettingsSource.TEST_ROLE + ":\n" + " cluster: [ all ]\n" + @@ -80,6 +84,9 @@ public class LicensingTests extends SecurityIntegTestCase { " indices:\n" + " - names: 'a'\n" + " privileges: [all]\n" + + " - names: 'test-dls'\n" + + " privileges: [read]\n" + + " query: '{\"term\":{\"field\":\"value\"} }'\n" + "\n" + "role_b:\n" + " indices:\n" + @@ -99,8 +106,8 @@ protected String configRoles() { @Override protected String configUsers() { return SecuritySettingsSource.CONFIG_STANDARD_USER + - "user_a:{plain}passwd\n" + - "user_b:{plain}passwd\n"; + "user_a:" + HASH_PASSWD + "\n" + + "user_b:" + HASH_PASSWD + "\n"; } @Override @@ -204,22 +211,24 @@ public void testNodeJoinWithoutSecurityExplicitlyEnabled() throws Exception { } public void testWarningHeader() throws Exception { - Request request = new Request("GET", "/_security/user"); + License.OperationMode mode = randomFrom(License.OperationMode.PLATINUM, License.OperationMode.ENTERPRISE); + enableLicensing(mode); + + // We test with "user_a" (that has a DLS-enabled role), so that we exercise licensed functionality and generate the warning header. + // Functionality under a basic (or SSPL) license does not necessarily generate expiration warnings + Request request = new Request("GET", "/_security/_authenticate"); RequestOptions.Builder options = request.getOptions().toBuilder(); - options.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, - new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); + options.addHeader("Authorization", basicAuthHeaderValue("user_a", new SecureString("passwd".toCharArray()))); request.setOptions(options); Response response = getRestClient().performRequest(request); List beforeWarningHeaders = getWarningHeaders(response.getHeaders()); assertTrue(beforeWarningHeaders.isEmpty()); - License.OperationMode mode = randomFrom(License.OperationMode.GOLD, License.OperationMode.PLATINUM, - License.OperationMode.ENTERPRISE, License.OperationMode.STANDARD); long now = System.currentTimeMillis(); long newExpirationDate = now + LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() - 1; setLicensingExpirationDate(mode, newExpirationDate); response = getRestClient().performRequest(request); - List afterWarningHeaders= getWarningHeaders(response.getHeaders()); + List afterWarningHeaders = getWarningHeaders(response.getHeaders()); assertThat(afterWarningHeaders, Matchers.hasSize(1)); assertThat(afterWarningHeaders.get(0), Matchers.containsString("Your license will expire in [6] days. " + "Contact your administrator or update your license for continued use of features")); @@ -244,7 +253,7 @@ public void testWarningHeader() throws Exception { "Contact your administrator or update your license for continued use of features")); } - public void testNoWarningHeaderWhenAuthenticationFailed() throws Exception { + public void testNoWarningHeaderWhenAuthenticationFailed() throws Exception { Request request = new Request("GET", "/_security/user"); RequestOptions.Builder options = request.getOptions().toBuilder(); options.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, 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 98bf18adb06e0..d4563e45d5542 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 @@ -53,6 +53,7 @@ import org.elasticsearch.ingest.Processor; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseService; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.plugins.ClusterPlugin; @@ -344,6 +345,30 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, public static final String SECURITY_CRYPTO_THREAD_POOL_NAME = XPackField.SECURITY + "-crypto"; + public static final LicensedFeature.Momentary IP_FILTERING_FEATURE = + LicensedFeature.momentaryLenient("security_ip_filtering", License.OperationMode.GOLD); + public static final LicensedFeature AUDITING_FEATURE = + LicensedFeature.momentaryLenient("security_auditing", License.OperationMode.GOLD); + public static final LicensedFeature DLS_FLS_FEATURE = + LicensedFeature.momentaryLenient("security_dls_fls", License.OperationMode.PLATINUM); + + // Builtin realms (file/native) realms are Basic licensed, so don't need to be checked or tracked + // Standard realms (LDAP, AD, PKI, etc) are Gold+ + // SSO realms are Platinum+ + public static final LicensedFeature.Persistent STANDARD_REALMS_FEATURE = + LicensedFeature.persistentLenient("security_standard_realms", License.OperationMode.GOLD); + public static final LicensedFeature.Persistent ALL_REALMS_FEATURE = + LicensedFeature.persistentLenient("security_all_realms", License.OperationMode.PLATINUM); + + public static final LicensedFeature CUSTOM_ROLE_FEATURE = + LicensedFeature.momentary("security_custom_role_providers", License.OperationMode.PLATINUM); + public static final LicensedFeature TOKEN_SERVICE_FEATURE = + LicensedFeature.momentaryLenient("security_token_service", License.OperationMode.STANDARD); + public static final LicensedFeature AUTH_REALM_FEATURE = + LicensedFeature.momentary("security_authorization_realm", License.OperationMode.PLATINUM); + public static final LicensedFeature AUTH_ENGINE_FEATURE = + LicensedFeature.momentary("security_authorization_engine", License.OperationMode.PLATINUM); + private static final Logger logger = LogManager.getLogger(Security.class); public static final SystemIndexDescriptor SECURITY_MAIN_INDEX_DESCRIPTOR = getSecurityMainIndexDescriptor(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index 438eea7b642d3..095a93c8912e2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -91,6 +91,10 @@ static boolean isStandardRealm(String type) { return STANDARD_TYPES.contains(type); } + static boolean isBuiltinRealm(String type) { + return FileRealmSettings.TYPE.equals(type) || NativeRealmSettings.TYPE.equals(type); + } + /** * Creates {@link Realm.Factory factories} for each internal realm type. * This excludes the {@link ReservedRealm}, as it cannot be created dynamically. @@ -147,4 +151,5 @@ public static List getBootstrapChecks(final Settings globalSetti .collect(Collectors.toList()); return checks; } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java index 86aa688aa7a7b..2dd6e5b6aca58 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java @@ -19,13 +19,13 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import java.util.ArrayList; @@ -87,7 +87,7 @@ public Realms(Settings settings, Environment env, Map fac standardRealms.add(realm); } - if (FileRealmSettings.TYPE.equals(realm.type()) || NativeRealmSettings.TYPE.equals(realm.type())) { + if (InternalRealms.isBuiltinRealm(realm.type())) { basicRealms.add(realm); } } @@ -119,7 +119,7 @@ public List getUnlicensedRealms() { } // If all realms are allowed, then nothing is unlicensed - if (licenseStateSnapshot.checkFeature(Feature.SECURITY_ALL_REALMS)) { + if (Security.ALL_REALMS_FEATURE.checkWithoutTracking(licenseStateSnapshot)) { return Collections.emptyList(); } @@ -130,8 +130,7 @@ public List getUnlicensedRealms() { } // Otherwise, we return anything in "all realms" that is not in the allowed realm list - List unlicensed = realms.stream().filter(r -> allowedRealms.contains(r) == false).collect(Collectors.toList()); - return Collections.unmodifiableList(unlicensed); + return realms.stream().filter(r -> allowedRealms.contains(r) == false).collect(Collectors.toUnmodifiableList()); } public Stream stream() { @@ -139,18 +138,26 @@ public Stream stream() { } public List asList() { + // TODO : Recalculate this when the license changes rather than on every call final XPackLicenseState licenseStateSnapshot = licenseState.copyCurrentLicenseState(); if (licenseStateSnapshot.isSecurityEnabled() == false) { return Collections.emptyList(); } - if (licenseStateSnapshot.checkFeature(Feature.SECURITY_ALL_REALMS)) { - return realms; - } else if (licenseStateSnapshot.checkFeature(Feature.SECURITY_STANDARD_REALMS)) { - return standardRealmsOnly; - } else { - // native realms are basic licensed, and always allowed, even for an expired license - return nativeRealmsOnly; + List licensed = realms.stream().filter(r -> checkLicense(r, licenseState)).collect(Collectors.toUnmodifiableList()); + return licensed; + } + + private static boolean checkLicense(Realm realm, XPackLicenseState licenseState) { + if (ReservedRealm.TYPE.equals(realm.type())) { + return true; + } + if (InternalRealms.isBuiltinRealm(realm.type())) { + return true; + } + if (InternalRealms.isStandardRealm(realm.type())) { + return Security.STANDARD_REALMS_FEATURE.checkAndStartTracking(licenseState, realm.name()); } + return Security.ALL_REALMS_FEATURE.checkAndStartTracking(licenseState, realm.name()); } public Realm realm(String name) { @@ -317,7 +324,7 @@ private List buildRealmConfigs() { throw new IllegalArgumentException("unknown realm type [" + identifier.getType() + "] for realm [" + identifier + "]"); } RealmConfig config = new RealmConfig(identifier, settings, env, threadContext); - if (FileRealmSettings.TYPE.equals(identifier.getType()) || NativeRealmSettings.TYPE.equals(identifier.getType())) { + if (InternalRealms.isBuiltinRealm(identifier.getType())) { // this is an internal realm factory, let's make sure we didn't already registered one // (there can only be one instance of an internal realm) if (internalTypes.contains(identifier.getType())) { @@ -341,7 +348,7 @@ private List buildRealmConfigs() { private Set findDisabledBasicRealmTypes(List realmConfigs) { return realmConfigs.stream() - .filter(rc -> FileRealmSettings.TYPE.equals(rc.type()) || NativeRealmSettings.TYPE.equals(rc.type())) + .filter(rc -> InternalRealms.isBuiltinRealm(rc.type())) .filter(rc -> false == rc.enabled()) .map(RealmConfig::type) .collect(Collectors.toUnmodifiableSet()); @@ -384,12 +391,12 @@ private static Map convertToMapOfLists(Map map) } public static boolean isRealmTypeAvailable(XPackLicenseState licenseState, String type) { - if (licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)) { + if (Security.ALL_REALMS_FEATURE.checkWithoutTracking(licenseState)) { return true; - } else if (licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)) { + } else if (Security.STANDARD_REALMS_FEATURE.checkWithoutTracking(licenseState)) { return InternalRealms.isStandardRealm(type) || ReservedRealm.TYPE.equals(type); } else { - return FileRealmSettings.TYPE.equals(type) || NativeRealmSettings.TYPE.equals(type); + return InternalRealms.isBuiltinRealm(type); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java index bf5219c89be7f..98c0046bb8d08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java @@ -18,8 +18,8 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.transport.TransportSettings; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; @@ -246,7 +246,7 @@ private void setHttpFiltering(boolean enabled) { public boolean accept(String profile, InetSocketAddress peerAddress) { if (licenseState.isSecurityEnabled() == false || - licenseState.checkFeature(Feature.SECURITY_IP_FILTERING) == false) { + Security.IP_FILTERING_FEATURE.check(licenseState) == false) { return true; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 12eab49f80369..f682f6f8e1bae 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -51,6 +51,7 @@ import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.rest.RestRequest; @@ -88,6 +89,7 @@ import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; @@ -145,6 +147,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.same; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -215,8 +218,8 @@ public void init() throws Exception { .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) .put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true) .build(); - XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(true); + MockLicenseState licenseState = mock(MockLicenseState.class); + when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(true); when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE)).thenReturn(true); when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); @@ -447,8 +450,9 @@ public void testAuthenticateSmartRealmOrdering() { verify(auditTrail).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest); verify(firstRealm, times(2)).name(); // used above one time - verify(secondRealm, times(2)).name(); - verify(secondRealm, times(2)).type(); // used to create realm ref + verify(firstRealm, atLeastOnce()).type(); + verify(secondRealm, Mockito.atLeast(3)).name(); // also used in license tracking + verify(secondRealm, Mockito.atLeast(3)).type(); // used to create realm ref, and license tracking verify(firstRealm, times(2)).token(threadContext); verify(secondRealm, times(2)).token(threadContext); verify(firstRealm).supports(token); @@ -572,8 +576,9 @@ public void testAuthenticateSmartRealmOrderingDisabled() { }, this::logAndFail)); verify(auditTrail, times(2)).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest); verify(firstRealm, times(3)).name(); // used above one time - verify(secondRealm, times(2)).name(); - verify(secondRealm, times(2)).type(); // used to create realm ref + verify(firstRealm, atLeastOnce()).type(); + verify(secondRealm, Mockito.atLeast(3)).name(); + verify(secondRealm, Mockito.atLeast(3)).type(); // used to create realm ref verify(firstRealm, times(2)).token(threadContext); verify(secondRealm, times(2)).token(threadContext); verify(firstRealm, times(2)).supports(token); @@ -636,8 +641,10 @@ public void testAuthenticateCached() throws Exception { assertThat(result.v1(), is(authentication)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.REALM)); verifyZeroInteractions(auditTrail); - verifyZeroInteractions(firstRealm); - verifyZeroInteractions(secondRealm); + verify(firstRealm, atLeastOnce()).type(); + verify(secondRealm, atLeastOnce()).type(); + verify(secondRealm, atLeastOnce()).name(); // This realm is license-tracked, which uses the name + verifyNoMoreInteractions(firstRealm, secondRealm); verifyZeroInteractions(operatorPrivilegesService); } @@ -917,7 +924,9 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { verifyZeroInteractions(operatorPrivilegesService); }, this::logAndFail)); assertTrue(completed.compareAndSet(true, false)); - verifyZeroInteractions(firstRealm); + verify(firstRealm, atLeastOnce()).type(); + verify(firstRealm, atLeastOnce()).name(); + verifyNoMoreInteractions(firstRealm); reset(firstRealm); } finally { terminate(threadPool1); @@ -965,7 +974,9 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { verifyZeroInteractions(operatorPrivilegesService); }, this::logAndFail)); assertTrue(completed.get()); - verifyZeroInteractions(firstRealm); + verify(firstRealm, atLeastOnce()).type(); + verify(firstRealm, atLeastOnce()).name(); + verifyNoMoreInteractions(firstRealm); } finally { terminate(threadPool2); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java index 266a7439216ff..524913e92ef88 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java @@ -13,8 +13,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; +import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -27,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.junit.Before; @@ -54,11 +54,12 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class RealmsTests extends ESTestCase { private Map factories; - private XPackLicenseState licenseState; + private MockLicenseState licenseState; private ThreadContext threadContext; private ReservedRealm reservedRealm; private int randomRealmTypesCount; @@ -74,7 +75,8 @@ public void init() throws Exception { String name = "type_" + i; factories.put(name, config -> new DummyRealm(name, config)); } - licenseState = mock(XPackLicenseState.class); + licenseState = mock(MockLicenseState.class); + when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); threadContext = new ThreadContext(Settings.EMPTY); reservedRealm = mock(ReservedRealm.class); @@ -85,18 +87,18 @@ public void init() throws Exception { } private void allowAllRealms() { - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)).thenReturn(true); + when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(true); + when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(true); } private void allowOnlyStandardRealms() { - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(false); - when(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)).thenReturn(true); + when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(false); + when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(true); } private void allowOnlyNativeRealms() { - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(false); - when(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)).thenReturn(false); + when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(false); + when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(false); } public void testWithSettings() throws Exception { @@ -258,6 +260,9 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), empty()); assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); + for (i = 0; i < randomRealmTypesCount; i++) { + verify(licenseState).enableUsageTracking(Security.ALL_REALMS_FEATURE, "realm_" + i); + } allowOnlyNativeRealms(); @@ -283,9 +288,10 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { public void testUnlicensedWithInternalRealms() throws Exception { factories.put(LdapRealmSettings.LDAP_TYPE, config -> new DummyRealm(LdapRealmSettings.LDAP_TYPE, config)); assertThat(factories.get("type_0"), notNullValue()); + String ldapRealmName = randomAlphaOfLengthBetween(3, 8); Settings.Builder builder = Settings.builder() .put("path.home", createTempDir()) - .put("xpack.security.authc.realms.ldap.foo.order", "0") + .put("xpack.security.authc.realms.ldap." + ldapRealmName + ".order", "0") .put("xpack.security.authc.realms.type_0.custom.order", "1"); final boolean fileRealmDisabled = randomDisableRealm(builder, FileRealmSettings.TYPE); final boolean nativeRealmDisabled = randomDisableRealm(builder, NativeRealmSettings.TYPE); @@ -306,6 +312,7 @@ public void testUnlicensedWithInternalRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), empty()); assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); + verify(licenseState).enableUsageTracking(Security.STANDARD_REALMS_FEATURE, ldapRealmName); allowOnlyStandardRealms(); iter = realms.iterator(); @@ -332,7 +339,7 @@ public void testUnlicensedWithInternalRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), iterableWithSize(2)); realm = realms.getUnlicensedRealms().get(0); assertThat(realm.type(), equalTo("ldap")); - assertThat(realm.name(), equalTo("foo")); + assertThat(realm.name(), equalTo(ldapRealmName)); realm = realms.getUnlicensedRealms().get(1); assertThat(realm.type(), equalTo("type_0")); assertThat(realm.name(), equalTo("custom")); @@ -367,6 +374,7 @@ public void testUnlicensedWithBasicRealmSettings() throws Exception { assertThat(realm.type(), is(type)); assertThat(iter.hasNext(), is(false)); assertThat(realms.getUnlicensedRealms(), empty()); + verify(licenseState).enableUsageTracking(Security.STANDARD_REALMS_FEATURE, "foo"); allowOnlyNativeRealms(); iter = realms.iterator(); @@ -392,9 +400,10 @@ public void testUnlicensedWithBasicRealmSettings() throws Exception { public void testUnlicensedWithNonStandardRealms() throws Exception { final String selectedRealmType = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE); factories.put(selectedRealmType, config -> new DummyRealm(selectedRealmType, config)); + String realmName = randomAlphaOfLengthBetween(3, 8); Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()) - .put("xpack.security.authc.realms." + selectedRealmType + ".foo.order", "0"); + .put("path.home", createTempDir()) + .put("xpack.security.authc.realms." + selectedRealmType + "." + realmName + ".order", "0"); final boolean fileRealmDisabled = randomDisableRealm(builder, FileRealmSettings.TYPE); final boolean nativeRealmDisabled = randomDisableRealm(builder, NativeRealmSettings.TYPE); Settings settings = builder.build(); @@ -409,6 +418,7 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { realm = iter.next(); assertThat(realm.type(), is(selectedRealmType)); assertThat(realms.getUnlicensedRealms(), empty()); + verify(licenseState).enableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); allowOnlyStandardRealms(); iter = realms.iterator(); @@ -420,7 +430,7 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), iterableWithSize(1)); realm = realms.getUnlicensedRealms().get(0); assertThat(realm.type(), equalTo(selectedRealmType)); - assertThat(realm.name(), equalTo("foo")); + assertThat(realm.name(), equalTo(realmName)); allowOnlyNativeRealms(); iter = realms.iterator(); @@ -432,7 +442,7 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), iterableWithSize(1)); realm = realms.getUnlicensedRealms().get(0); assertThat(realm.type(), equalTo(selectedRealmType)); - assertThat(realm.name(), equalTo("foo")); + assertThat(realm.name(), equalTo(realmName)); } public void testDisabledRealmsAreNotAdded() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java index 20324dff368d1..86b697729ee8d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java @@ -15,14 +15,15 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.license.TestUtils; import org.elasticsearch.node.MockNode; import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.junit.annotations.Network; import org.elasticsearch.transport.Transport; import org.elasticsearch.xpack.security.LocalStateSecurity; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.junit.Before; @@ -51,7 +52,7 @@ public class IPFilterTests extends ESTestCase { private IPFilter ipFilter; - private XPackLicenseState licenseState; + private MockLicenseState licenseState; private AuditTrail auditTrail; private AuditTrailService auditTrailService; private Transport transport; @@ -60,10 +61,10 @@ public class IPFilterTests extends ESTestCase { @Before public void init() { - licenseState = mock(XPackLicenseState.class); + licenseState = TestUtils.newMockLicenceState(); when(licenseState.isSecurityEnabled()).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING)).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_AUDITING)).thenReturn(true); + when(licenseState.isAllowed(Security.IP_FILTERING_FEATURE)).thenReturn(true); + when(licenseState.isAllowed(Security.AUDITING_FEATURE)).thenReturn(true); auditTrail = mock(AuditTrail.class); auditTrailService = new AuditTrailService(Collections.singletonList(auditTrail), licenseState); clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(Arrays.asList( @@ -253,7 +254,7 @@ public void testThatAllAddressesAreAllowedWhenLicenseDisablesSecurity() { Settings settings = Settings.builder() .put("xpack.security.transport.filter.deny", "_all") .build(); - when(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING)).thenReturn(false); + when(licenseState.isAllowed(Security.IP_FILTERING_FEATURE)).thenReturn(false); ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); @@ -264,7 +265,7 @@ public void testThatAllAddressesAreAllowedWhenLicenseDisablesSecurity() { verifyZeroInteractions(auditTrail); // for sanity enable license and check that it is denied - when(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING)).thenReturn(true); + when(licenseState.isAllowed(Security.IP_FILTERING_FEATURE)).thenReturn(true); ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java index 28293f06b7377..0153f2866d644 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java @@ -14,10 +14,11 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.license.TestUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.Before; @@ -57,9 +58,9 @@ public void init() throws Exception { IPFilter.TRANSPORT_FILTER_DENY_SETTING, IPFilter.PROFILE_FILTER_ALLOW_SETTING, IPFilter.PROFILE_FILTER_DENY_SETTING))); - XPackLicenseState licenseState = mock(XPackLicenseState.class); + MockLicenseState licenseState = TestUtils.newMockLicenceState(); when(licenseState.isSecurityEnabled()).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING)).thenReturn(true); + when(licenseState.isAllowed(Security.IP_FILTERING_FEATURE)).thenReturn(true); AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); IPFilter ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java index 72148c1b9bf3e..c12a655b74ccb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java @@ -13,11 +13,12 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.license.TestUtils; import org.elasticsearch.nio.NioChannelHandler; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.Transport; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.Before; @@ -60,9 +61,9 @@ public void init() throws Exception { IPFilter.TRANSPORT_FILTER_DENY_SETTING, IPFilter.PROFILE_FILTER_ALLOW_SETTING, IPFilter.PROFILE_FILTER_DENY_SETTING))); - XPackLicenseState licenseState = mock(XPackLicenseState.class); + MockLicenseState licenseState = TestUtils.newMockLicenceState(); when(licenseState.isSecurityEnabled()).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_IP_FILTERING)).thenReturn(true); + when(licenseState.isAllowed(Security.IP_FILTERING_FEATURE)).thenReturn(true); AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses());