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 753ae5de3b44c..c87020cfab8ba 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 @@ -45,8 +45,13 @@ public enum Feature { SECURITY_IP_FILTERING(OperationMode.GOLD, false), SECURITY_AUDITING(OperationMode.GOLD, false), SECURITY_DLS_FLS(OperationMode.PLATINUM, false), - SECURITY_ALL_REALMS(OperationMode.PLATINUM, false), - SECURITY_STANDARD_REALMS(OperationMode.GOLD, false), + SECURITY_LDAP_REALM(OperationMode.GOLD, false), + SECURITY_AD_REALM(OperationMode.GOLD, false), + SECURITY_PKI_REALM(OperationMode.GOLD, false), + SECURITY_SAML_REALM(OperationMode.PLATINUM, false), + SECURITY_OIDC_REALM(OperationMode.PLATINUM, false), + SECURITY_KERBEROS_REALM(OperationMode.PLATINUM, false), + SECURITY_CUSTOM_REALM(OperationMode.PLATINUM, false), SECURITY_CUSTOM_ROLE_PROVIDERS(OperationMode.PLATINUM, true), SECURITY_TOKEN_SERVICE(OperationMode.GOLD, false), SECURITY_API_KEY_SERVICE(OperationMode.MISSING, false), @@ -109,12 +114,10 @@ public enum Feature { } } - // 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, - Feature.SECURITY_ALL_REALMS, - Feature.SECURITY_STANDARD_REALMS + Feature.SECURITY_IP_FILTERING ); /** Messages for each feature which are printed when the license expires. */ 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 27222105f3e39..2fe817f4830b0 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 @@ -35,6 +35,7 @@ import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt; @@ -383,7 +384,11 @@ public UpdatableLicenseState() { } public UpdatableLicenseState(Settings settings) { - super(settings, () -> 0); + this(settings, () -> 0); + } + + public UpdatableLicenseState(Settings settings, LongSupplier epochMillis) { + super(settings, epochMillis); } @Override 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 dd521ce4d9ba3..36ad7f6414ad0 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 @@ -87,8 +87,16 @@ public void testSecurityDefaults() { assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_STATS_AND_HEALTH), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_DLS_FLS), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_AD_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_LDAP_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_PKI_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_SAML_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_OIDC_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_KERBEROS_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE), is(true)); licenseState = TestUtils.newTestLicenseState(); assertSecurityNotAllowed(licenseState); @@ -203,7 +211,13 @@ public void testSecurityGold() { assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_STATS_AND_HEALTH), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_DLS_FLS), is(false)); - assertThat(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_AD_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_LDAP_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_PKI_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_SAML_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_OIDC_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_KERBEROS_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE), is(true)); @@ -220,7 +234,13 @@ public void testSecurityGoldExpired() { assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_STATS_AND_HEALTH), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_DLS_FLS), is(false)); - assertThat(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_AD_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_LDAP_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_PKI_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_SAML_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_OIDC_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_KERBEROS_REALM), is(false)); + assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE), is(true)); @@ -237,7 +257,13 @@ public void testSecurityPlatinum() { assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_STATS_AND_HEALTH), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_DLS_FLS), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_AD_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_LDAP_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_PKI_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_SAML_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_OIDC_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_KERBEROS_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE), is(true)); @@ -254,7 +280,13 @@ public void testSecurityPlatinumExpired() { assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_STATS_AND_HEALTH), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_DLS_FLS), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_AD_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_LDAP_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_PKI_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_SAML_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_OIDC_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_KERBEROS_REALM), is(true)); + assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE), is(true)); 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 4e2a2362ded4d..e65499cf6f7cd 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 @@ -9,6 +9,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.env.Environment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -52,13 +53,18 @@ public final class InternalRealms { /** - * The list of all internal realm types, excluding {@link ReservedRealm#TYPE}. + * The map of all internal realm types, excluding {@link ReservedRealm#TYPE}, to their relevant license feature */ - private static final Set XPACK_TYPES = Collections - .unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, - LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, - OpenIdConnectRealmSettings.TYPE)); - + private static final Map XPACK_TYPES = Map.ofEntries( + Map.entry(NativeRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY), + Map.entry(FileRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY), + Map.entry(LdapRealmSettings.AD_TYPE, XPackLicenseState.Feature.SECURITY_AD_REALM), + Map.entry(LdapRealmSettings.LDAP_TYPE, XPackLicenseState.Feature.SECURITY_LDAP_REALM), + Map.entry(PkiRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY_PKI_REALM), + Map.entry(SamlRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY_SAML_REALM), + Map.entry(OpenIdConnectRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY_OIDC_REALM), + Map.entry(KerberosRealmSettings.TYPE, XPackLicenseState.Feature.SECURITY_KERBEROS_REALM) + ); /** * The list of all standard realm types, which are those provided by x-pack and do not have extensive * interaction with third party sources @@ -71,14 +77,14 @@ public final class InternalRealms { * including the {@link ReservedRealm} */ static boolean isXPackRealm(String type) { - if (XPACK_TYPES.contains(type)) { + if (XPACK_TYPES.containsKey(type)) { return true; } return ReservedRealm.TYPE.equals(type); } public static Collection getConfigurableRealmsTypes() { - return Collections.unmodifiableSet(XPACK_TYPES); + return XPACK_TYPES.keySet(); } /** @@ -102,34 +108,34 @@ public static Map getFactories(ThreadPool threadPool, Res SecurityIndexManager securityIndex) { return Map.of( - // file realm - FileRealmSettings.TYPE, - config -> new FileRealm(config, resourceWatcherService, threadPool), - // native realm - NativeRealmSettings.TYPE, - config -> { - final NativeRealm nativeRealm = new NativeRealm(config, nativeUsersStore, threadPool); - securityIndex.addIndexStateListener(nativeRealm::onSecurityIndexStateChange); - return nativeRealm; - }, - // active directory realm - LdapRealmSettings.AD_TYPE, - config -> new LdapRealm(config, sslService, resourceWatcherService, nativeRoleMappingStore, threadPool), - // LDAP realm - LdapRealmSettings.LDAP_TYPE, - config -> new LdapRealm(config, sslService, resourceWatcherService, nativeRoleMappingStore, threadPool), - // PKI realm - PkiRealmSettings.TYPE, - config -> new PkiRealm(config, resourceWatcherService, nativeRoleMappingStore), - // SAML realm - SamlRealmSettings.TYPE, - config -> SamlRealm.create(config, sslService, resourceWatcherService, nativeRoleMappingStore), - // Kerberos realm - KerberosRealmSettings.TYPE, - config -> new KerberosRealm(config, nativeRoleMappingStore, threadPool), - // OpenID Connect realm - OpenIdConnectRealmSettings.TYPE, - config -> new OpenIdConnectRealm(config, sslService, nativeRoleMappingStore, resourceWatcherService)); + // file realm + FileRealmSettings.TYPE, + config -> new FileRealm(config, resourceWatcherService, threadPool), + // native realm + NativeRealmSettings.TYPE, + config -> { + final NativeRealm nativeRealm = new NativeRealm(config, nativeUsersStore, threadPool); + securityIndex.addIndexStateListener(nativeRealm::onSecurityIndexStateChange); + return nativeRealm; + }, + // active directory realm + LdapRealmSettings.AD_TYPE, + config -> new LdapRealm(config, sslService, resourceWatcherService, nativeRoleMappingStore, threadPool), + // LDAP realm + LdapRealmSettings.LDAP_TYPE, + config -> new LdapRealm(config, sslService, resourceWatcherService, nativeRoleMappingStore, threadPool), + // PKI realm + PkiRealmSettings.TYPE, + config -> new PkiRealm(config, resourceWatcherService, nativeRoleMappingStore), + // SAML realm + SamlRealmSettings.TYPE, + config -> SamlRealm.create(config, sslService, resourceWatcherService, nativeRoleMappingStore), + // Kerberos realm + KerberosRealmSettings.TYPE, + config -> new KerberosRealm(config, nativeRoleMappingStore, threadPool), + // OpenID Connect realm + OpenIdConnectRealmSettings.TYPE, + config -> new OpenIdConnectRealm(config, sslService, nativeRoleMappingStore, resourceWatcherService)); } private InternalRealms() { @@ -146,4 +152,11 @@ public static List getBootstrapChecks(final Settings globalSetti .collect(Collectors.toList()); return checks; } + + public static XPackLicenseState.Feature getLicenseFeature(String type) { + if (ReservedRealm.TYPE.equals(type)) { + return XPackLicenseState.Feature.SECURITY; + } + return XPACK_TYPES.getOrDefault(type, XPackLicenseState.Feature.SECURITY_CUSTOM_REALM); + } } 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 d89b797dac2e7..406282a348397 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 @@ -15,7 +15,6 @@ 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; @@ -25,7 +24,6 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -53,12 +51,13 @@ public class Realms implements Iterable { private final ThreadContext threadContext; private final ReservedRealm reservedRealm; - protected List realms; - // a list of realms that are considered standard in that they are provided by x-pack and - // interact with a 3rd party source on a limited basis - List standardRealmsOnly; - // a list of realms that are considered native, that is they only interact with x-pack and no 3rd party auth sources - List nativeRealmsOnly; + // All realms that were explicitly configured in the settings, some of these may not be enabled due to licensing + private final List allConfiguredRealms; + // the default builtin realms to enable if no other realms are permitted under the current license + private final List fallbackRealms; + + // the realms in current use. This list will change dynamically as the license changes + private volatile List activeRealms; public Realms(Settings settings, Environment env, Map factories, XPackLicenseState licenseState, ThreadContext threadContext, ReservedRealm reservedRealm) throws Exception { @@ -69,35 +68,47 @@ public Realms(Settings settings, Environment env, Map fac this.threadContext = threadContext; this.reservedRealm = reservedRealm; assert factories.get(ReservedRealm.TYPE) == null; - this.realms = initRealms(); - // pre-computing a list of internal only realms allows us to have much cheaper iteration than a custom iterator - // and is also simpler in terms of logic. These lists are small, so the duplication should not be a real issue here - List standardRealms = new ArrayList<>(); - List nativeRealms = new ArrayList<>(); - for (Realm realm : realms) { - // don't add the reserved realm here otherwise we end up with only this realm... - if (InternalRealms.isStandardRealm(realm.type())) { - standardRealms.add(realm); - } - if (FileRealmSettings.TYPE.equals(realm.type()) || NativeRealmSettings.TYPE.equals(realm.type())) { - nativeRealms.add(realm); - } - } + this.allConfiguredRealms = initRealms(); + this.fallbackRealms = buildFallbackRealms(reservedRealm); + this.activeRealms = allConfiguredRealms; - for (List realmList : Arrays.asList(standardRealms, nativeRealms)) { - if (realmList.isEmpty()) { - addNativeRealms(realmList); - } + this.allConfiguredRealms.forEach(r -> r.initialize(allConfiguredRealms, licenseState)); + this.fallbackRealms.stream().filter(r -> r != reservedRealm).forEach(r -> r.initialize(fallbackRealms, licenseState)); + + licenseState.addListener(this::recomputeActiveRealms); + recomputeActiveRealms(); + } - assert realmList.contains(reservedRealm) == false; - realmList.add(0, reservedRealm); - assert realmList.get(0) == reservedRealm; + protected void recomputeActiveRealms() { + final XPackLicenseState license = licenseState.copyCurrentLicenseState(); + final List licensedRealms = calculateLicensedRealms(license); + if (hasConfigurableRealms(licensedRealms)) { + activeRealms = licensedRealms; + } else { + activeRealms = fallbackRealms; + } + if (logger.isDebugEnabled()) { + logger.debug("license state has changed to [{}]; configured realms are: [{}]; active realms are: [{}]", + license.getOperationMode(), allConfiguredRealms, activeRealms); + } + } + + private static boolean hasConfigurableRealms(List realmList) { + if (realmList.isEmpty()) { + return false; + } + if (realmList.size() == 1 && ReservedRealm.TYPE.equals(realmList.get(0).type())) { + return false; } + return true; + } - this.standardRealmsOnly = Collections.unmodifiableList(standardRealms); - this.nativeRealmsOnly = Collections.unmodifiableList(nativeRealms); - realms.forEach(r -> r.initialize(this, licenseState)); + // Protected for testing + protected List calculateLicensedRealms(XPackLicenseState license) { + return allConfiguredRealms.stream() + .filter(r -> isRealmTypeAvailable(license, r.type())) + .collect(Collectors.toUnmodifiableList()); } @Override @@ -109,26 +120,15 @@ public Iterator iterator() { * Returns a list of realms that are configured, but are not permitted under the current license. */ public List getUnlicensedRealms() { - final XPackLicenseState licenseStateSnapshot = licenseState.copyCurrentLicenseState(); - // If auth is not allowed, then everything is unlicensed - if (licenseStateSnapshot.isSecurityEnabled() == false) { - return Collections.unmodifiableList(realms); - } - - // If all realms are allowed, then nothing is unlicensed - if (licenseStateSnapshot.checkFeature(Feature.SECURITY_ALL_REALMS)) { - return Collections.emptyList(); - } - - final List allowedRealms = this.asList(); - // Shortcut for the typical case, all the configured realms are allowed - if (allowedRealms.equals(this.realms)) { + final List activeSnapshot = activeRealms; + if (activeSnapshot.equals(allConfiguredRealms)) { return Collections.emptyList(); } // 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 allConfiguredRealms.stream() + .filter(r -> activeSnapshot.contains(r) == false) + .collect(Collectors.toUnmodifiableList()); } public Stream stream() { @@ -136,22 +136,15 @@ public Stream stream() { } public List asList() { - final XPackLicenseState licenseStateSnapshot = licenseState.copyCurrentLicenseState(); - if (licenseStateSnapshot.isSecurityEnabled() == false) { + if (licenseState.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; - } + assert activeRealms != null : "Active realms not configured"; + return activeRealms; } public Realm realm(String name) { - for (Realm realm : realms) { + for (Realm realm : activeRealms) { if (name.equals(realm.name())) { return realm; } @@ -170,7 +163,7 @@ protected List initRealms() throws Exception { List kerberosRealmNames = new ArrayList<>(); Map> nameToRealmIdentifier = new HashMap<>(); Map> orderToRealmName = new HashMap<>(); - for (RealmConfig.RealmIdentifier identifier: realmsSettings.keySet()) { + for (RealmConfig.RealmIdentifier identifier : realmsSettings.keySet()) { Realm.Factory factory = factories.get(identifier.getType()); if (factory == null) { throw new IllegalArgumentException("unknown realm type [" + identifier.getType() + "] for realm [" + identifier + "]"); @@ -187,7 +180,7 @@ protected List initRealms() throws Exception { // (there can only be one instance of an internal realm) if (internalTypes.contains(identifier.getType())) { throw new IllegalArgumentException("multiple [" + identifier.getType() + "] realms are configured. [" - + identifier.getType() + "] is an internal realm and therefore there can only be one such realm configured"); + + identifier.getType() + "] is an internal realm and therefore there can only be one such realm configured"); } internalTypes.add(identifier.getType()); } @@ -227,6 +220,13 @@ protected List initRealms() throws Exception { return Collections.unmodifiableList(realms); } + protected List buildFallbackRealms(ReservedRealm reservedRealm) throws Exception { + List fallback = new ArrayList<>(3); + fallback.add(reservedRealm); + addNativeRealms(fallback); + return List.copyOf(fallback); + } + public void usageStats(ActionListener> listener) { final XPackLicenseState licenseStateSnapshot = licenseState.copyCurrentLicenseState(); Map realmMap = new HashMap<>(); @@ -289,7 +289,7 @@ public void usageStats(ActionListener> listener) { } } - private void addNativeRealms(List realms) throws Exception { + private List addNativeRealms(List realms) throws Exception { Realm.Factory fileRealm = factories.get(FileRealmSettings.TYPE); if (fileRealm != null) { var realmIdentifier = new RealmConfig.RealmIdentifier(FileRealmSettings.TYPE, "default_" + FileRealmSettings.TYPE); @@ -306,6 +306,7 @@ private void addNativeRealms(List realms) throws Exception { ensureOrderSetting(settings, realmIdentifier, Integer.MIN_VALUE + 2), env, threadContext))); } + return realms; } private Settings ensureOrderSetting(Settings settings, RealmConfig.RealmIdentifier realmIdentifier, int order) { @@ -346,13 +347,11 @@ private static Map convertToMapOfLists(Map map) } public static boolean isRealmTypeAvailable(XPackLicenseState licenseState, String type) { - if (licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)) { - return true; - } else if (licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)) { - return InternalRealms.isStandardRealm(type) || ReservedRealm.TYPE.equals(type); - } else { - return FileRealmSettings.TYPE.equals(type) || NativeRealmSettings.TYPE.equals(type); - } + XPackLicenseState.Feature feature = InternalRealms.getLicenseFeature(type); + boolean allowed = licenseState.checkFeature(feature); + logger.trace("Realm type [{}] is associated with feature [{}] which {} allowed on license [{}]", + type, feature, allowed ? "is" : "isn't", licenseState.getOperationMode()); + return allowed; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java index 0ebeb35bbab1f..0212a72516169 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; @@ -25,6 +24,7 @@ import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.security.action.TransportDelegatePkiAuthenticationAction; +import org.elasticsearch.xpack.security.authc.Realms; import java.io.IOException; import java.util.List; @@ -54,7 +54,7 @@ protected Exception checkFeatureAvailable(RestRequest request) { Exception failedFeature = super.checkFeatureAvailable(request); if (failedFeature != null) { return failedFeature; - } else if (licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)) { + } else if (Realms.isRealmTypeAvailable(licenseState, PkiRealmSettings.TYPE)) { return null; } else { logger.info("The '{}' realm is not available under the current license", PkiRealmSettings.TYPE); 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 eb3c76523ea36..97bd31bfceb29 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 @@ -49,6 +49,8 @@ import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.license.License; +import org.elasticsearch.license.TestUtils; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.rest.RestRequest; @@ -129,6 +131,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; @@ -198,19 +201,19 @@ 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); - when(licenseState.isSecurityEnabled()).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_API_KEY_SERVICE)).thenReturn(true); - when(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE)).thenReturn(true); - when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); - when(licenseState.checkFeature(Feature.SECURITY_AUDITING)).thenReturn(true); + + TestUtils.UpdatableLicenseState licenseState = new TestUtils.UpdatableLicenseState(); + licenseState.update(License.OperationMode.ENTERPRISE, true, null); + ReservedRealm reservedRealm = mock(ReservedRealm.class); when(reservedRealm.type()).thenReturn("reserved"); when(reservedRealm.name()).thenReturn("reserved_realm"); - realms = spy(new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), Collections.emptyMap(), - licenseState, threadContext, reservedRealm, Arrays.asList(firstRealm, secondRealm), - Collections.singletonList(firstRealm))); + List realmList = Arrays.asList(firstRealm, secondRealm); + this.realms = spy(new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), Collections.emptyMap(), + licenseState, threadContext, reservedRealm, realmList, Collections.singletonList(firstRealm))); + this.realms.recomputeActiveRealms(); + // Verify we're configured correctly, otherwise everything else will fail + assertThat(this.realms.asList(), equalTo(realmList)); auditTrail = mock(AuditTrail.class); auditTrailService = new AuditTrailService(Collections.singletonList(auditTrail), licenseState); @@ -256,7 +259,7 @@ public void init() throws Exception { apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, licenseState, securityIndex, clusterService, threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); - service = new AuthenticationService(settings, realms, auditTrailService, + service = new AuthenticationService(settings, this.realms, auditTrailService, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), tokenService, apiKeyService); } @@ -340,8 +343,7 @@ public void testAuthenticateBothSupportSecondSucceeds() throws Exception { }, this::logAndFail)); assertTrue(completed.get()); verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", transportRequest); - verify(realms).asList(); - verifyNoMoreInteractions(realms); + verify(realms, atLeastOnce()).asList(); } public void testAuthenticateSmartRealmOrdering() { @@ -1594,12 +1596,33 @@ private static void mockRealmLookupReturnsNull(Realm realm, String username) { static class TestRealms extends Realms { + private final List allRealms; + private final List internalRealms; + TestRealms(Settings settings, Environment env, Map factories, XPackLicenseState licenseState, ThreadContext threadContext, ReservedRealm reservedRealm, List realms, List internalRealms) throws Exception { super(settings, env, factories, licenseState, threadContext, reservedRealm); - this.realms = realms; - this.standardRealmsOnly = internalRealms; + this.allRealms = realms; + this.internalRealms = internalRealms; + } + + @Override + protected List calculateLicensedRealms(XPackLicenseState license) { + if (allRealms == null) { + // This can happen because the realms are recalculated during construction + return super.calculateLicensedRealms(license); + } + if (license.checkFeature(Feature.SECURITY_CUSTOM_REALM)) { + return allRealms; + } else { + return internalRealms; + } + } + + // Make public for testing + public void recomputeActiveRealms() { + super.recomputeActiveRealms(); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java index 03b355497ec31..f947400191bf5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java @@ -9,6 +9,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.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; @@ -24,6 +25,7 @@ import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -60,11 +62,19 @@ public void testNativeRealmRegistersIndexHealthChangeListener() throws Exception verify(securityIndex, times(2)).addIndexStateListener(isA(BiConsumer.class)); } - public void testIsStandardType() { - String type = randomFrom(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, - PkiRealmSettings.TYPE); - assertThat(InternalRealms.isStandardRealm(type), is(true)); - type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE); - assertThat(InternalRealms.isStandardRealm(type), is(false)); + public void testLicenseChecks() { + assertThat(InternalRealms.getLicenseFeature(ReservedRealm.TYPE), is(XPackLicenseState.Feature.SECURITY)); + assertThat(InternalRealms.getLicenseFeature(NativeRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY)); + assertThat(InternalRealms.getLicenseFeature(FileRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY)); + + assertThat(InternalRealms.getLicenseFeature(LdapRealmSettings.AD_TYPE), is(XPackLicenseState.Feature.SECURITY_AD_REALM)); + assertThat(InternalRealms.getLicenseFeature(LdapRealmSettings.LDAP_TYPE), is(XPackLicenseState.Feature.SECURITY_LDAP_REALM)); + assertThat(InternalRealms.getLicenseFeature(PkiRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY_PKI_REALM)); + + assertThat(InternalRealms.getLicenseFeature(SamlRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY_SAML_REALM)); + assertThat(InternalRealms.getLicenseFeature(OpenIdConnectRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY_OIDC_REALM)); + assertThat(InternalRealms.getLicenseFeature(KerberosRealmSettings.TYPE), is(XPackLicenseState.Feature.SECURITY_KERBEROS_REALM)); + + assertThat(InternalRealms.getLicenseFeature(randomAlphaOfLength(12)), is(XPackLicenseState.Feature.SECURITY_CUSTOM_REALM)); } } 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 0bf6e2eb0b4d7..bf082429e2825 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 @@ -7,10 +7,12 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.LicenseStateListener; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.test.ESTestCase; @@ -23,13 +25,16 @@ import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.junit.Before; +import org.mockito.Mockito; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -37,7 +42,11 @@ import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; @@ -49,7 +58,9 @@ import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class RealmsTests extends ESTestCase { @@ -58,6 +69,7 @@ public class RealmsTests extends ESTestCase { private ThreadContext threadContext; private ReservedRealm reservedRealm; private int randomRealmTypesCount; + private List licenseStateListeners; @Before public void init() throws Exception { @@ -70,8 +82,19 @@ public void init() throws Exception { String name = "type_" + i; factories.put(name, config -> new DummyRealm(name, config)); } + logger.info("Created [{}] ([{}] with custom type) factories: [{}]", + factories.size(), randomRealmTypesCount, Strings.collectionToCommaDelimitedString(factories.keySet())); licenseState = mock(XPackLicenseState.class); + licenseStateListeners = new ArrayList<>(); when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); + Mockito.doAnswer(inv -> { + assertThat(inv.getArguments(), arrayWithSize(1)); + Object arg0 = inv.getArguments()[0]; + assertThat(arg0, instanceOf(LicenseStateListener.class)); + this.licenseStateListeners.add((LicenseStateListener) arg0); + return null; + }).when(licenseState).addListener(Mockito.any(LicenseStateListener.class)); + threadContext = new ThreadContext(Settings.EMPTY); reservedRealm = mock(ReservedRealm.class); when(licenseState.isSecurityEnabled()).thenReturn(true); @@ -81,18 +104,35 @@ 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.checkFeature(Feature.SECURITY_CUSTOM_REALM)).thenReturn(true); + allowRealms(InternalRealms.getConfigurableRealmsTypes()); } private void allowOnlyStandardRealms() { - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(false); - when(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)).thenReturn(true); + when(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM)).thenReturn(false); + allowRealms(List.of( + LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, + NativeRealmSettings.TYPE, FileRealmSettings.TYPE + )); } private void allowOnlyNativeRealms() { - when(licenseState.checkFeature(Feature.SECURITY_ALL_REALMS)).thenReturn(false); - when(licenseState.checkFeature(Feature.SECURITY_STANDARD_REALMS)).thenReturn(false); + when(licenseState.checkFeature(Feature.SECURITY_CUSTOM_REALM)).thenReturn(false); + allowRealms(List.of(NativeRealmSettings.TYPE, FileRealmSettings.TYPE)); + } + + private void allowRealms(Collection types) { + for (String type : InternalRealms.getConfigurableRealmsTypes()) { + Feature feature = InternalRealms.getLicenseFeature(type); + if (feature == Feature.SECURITY || types.contains(type)) { + logger.debug("Setting feature [{}] (realm [{}]) on [{}] to allowed", feature, type, licenseState); + when(licenseState.checkFeature(feature)).thenReturn(true); + } else { + logger.debug("Setting feature [{}] (realm [{}]) on [{}] to not-allowed", feature, type, licenseState); + when(licenseState.checkFeature(feature)).thenReturn(false); + } + } + licenseStateListeners.forEach(LicenseStateListener::licenseStateChanged); } public void testWithSettings() throws Exception { @@ -223,6 +263,11 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { Environment env = TestEnvironment.newEnvironment(settings); Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); + allowAllRealms(); + // We want to trigger license feature recording during node startup (when the realms are first created). + verify(licenseState, atLeastOnce()).checkFeature(Feature.SECURITY); + verify(licenseState, atLeastOnce()).checkFeature(Feature.SECURITY_CUSTOM_REALM); + // this is the iterator when licensed Iterator iter = realms.iterator(); assertThat(iter.hasNext(), is(true)); @@ -242,49 +287,7 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); allowOnlyNativeRealms(); - - iter = realms.iterator(); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm, is(reservedRealm)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), equalTo(FileRealmSettings.TYPE)); - assertThat(realm.name(), equalTo("default_" + FileRealmSettings.TYPE)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE)); - assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE)); - assertThat(iter.hasNext(), is(false)); - - assertThat(realms.getUnlicensedRealms(), iterableWithSize(randomRealmTypesCount)); - iter = realms.getUnlicensedRealms().iterator(); - i = 0; - while (iter.hasNext()) { - realm = iter.next(); - assertThat(realm.order(), equalTo(i)); - int index = orderToIndex.get(i); - assertThat(realm.type(), equalTo("type_" + index)); - assertThat(realm.name(), equalTo("realm_" + index)); - i++; - } - - allowOnlyNativeRealms(); - - iter = realms.iterator(); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm, is(reservedRealm)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), equalTo(FileRealmSettings.TYPE)); - assertThat(realm.name(), equalTo("default_" + FileRealmSettings.TYPE)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE)); - assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE)); - assertThat(iter.hasNext(), is(false)); - + assertIterator(realms.iterator(), Realm::type, List.of(ReservedRealm.TYPE, FileRealmSettings.TYPE, NativeRealmSettings.TYPE)); assertThat(realms.getUnlicensedRealms(), iterableWithSize(randomRealmTypesCount)); iter = realms.getUnlicensedRealms().iterator(); i = 0; @@ -377,31 +380,16 @@ public void testUnlicensedWithNativeRealmSettings() throws Exception { Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); - Iterator iter = realms.iterator(); - assertThat(iter.hasNext(), is(true)); - Realm realm = iter.next(); - assertThat(realm, is(reservedRealm)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), is("ldap")); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), is(type)); - assertThat(iter.hasNext(), is(false)); - assertThat(realms.getUnlicensedRealms(), empty()); + + assertIterator(realms.iterator(), Realm::type, List.of(ReservedRealm.TYPE, "ldap", type)); + + checkUnlicensedRealmCount(realms, 0); allowOnlyNativeRealms(); - iter = realms.iterator(); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm, is(reservedRealm)); - assertThat(iter.hasNext(), is(true)); - realm = iter.next(); - assertThat(realm.type(), is(type)); - assertThat(iter.hasNext(), is(false)); + assertIterator(realms.iterator(), Realm::type, List.of(ReservedRealm.TYPE, type)); - assertThat(realms.getUnlicensedRealms(), iterableWithSize(1)); - realm = realms.getUnlicensedRealms().get(0); + checkUnlicensedRealmCount(realms, 1); + Realm realm = realms.getUnlicensedRealms().get(0); assertThat(realm.type(), equalTo("ldap")); assertThat(realm.name(), equalTo("foo")); } @@ -477,7 +465,7 @@ public void testDisabledRealmsAreNotAdded() throws Exception { builder.put("xpack.security.authc.realms.type_" + i + ".realm_" + i + ".enabled", enabled); if (enabled) { orderToIndex.put(orders.get(i), i); - logger.error("put [{}] -> [{}]", orders.get(i), i); + logger.info("enabled realm [{}] at index [{}]", orders.get(i), i); } } Settings settings = builder.build(); @@ -514,7 +502,7 @@ public void testDisabledRealmsAreNotAdded() throws Exception { // check that disabled realms are not included in unlicensed realms allowOnlyNativeRealms(); - assertThat(realms.getUnlicensedRealms(), hasSize(orderToIndex.size())); + checkUnlicensedRealmCount(realms, orderToIndex.size()); } public void testAuthcAuthzDisabled() throws Exception { @@ -528,7 +516,7 @@ public void testAuthcAuthzDisabled() throws Exception { assertThat(realms.iterator().hasNext(), is(true)); when(licenseState.isSecurityEnabled()).thenReturn(false); - assertThat(realms.iterator().hasNext(), is(false)); + assertIterator(realms.iterator(), Function.identity(), List.of()); } public void testUsageStats() throws Exception { @@ -600,6 +588,40 @@ public void testInitRealmsFailsForMultipleKerberosRealms() throws IOException { "multiple realms [realm_1, realm_2] configured of type [kerberos], [kerberos] can only have one such realm configured"))); } + public void testInitMultipleRealms() throws Exception { + final Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); + builder.put("xpack.security.authc.realms.file.realm_1.order", 1); + builder.put("xpack.security.authc.realms.native.realm_2.order", 2); + builder.put("xpack.security.authc.realms.kerberos.realm_3.order", 3); + + final Settings settings = builder.build(); + Environment env = TestEnvironment.newEnvironment(settings); + Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); + + assertThat(realms.asList(), hasSize(4)); // reserved + 3 configured realms + } + + private void checkUnlicensedRealmCount(Realms realms, int expectedSize) { + List unlicensedRealms = realms.getUnlicensedRealms(); + assertThat("Unlicensed realms are: " + unlicensedRealms.stream().map(Realm::name).collect(Collectors.joining(",")), + realms.getUnlicensedRealms(), hasSize(expectedSize)); + } + + private void assertIterator(Iterator iterator, Function property, List values) { + for (int i = 0; i < values.size(); i++) { + assertThat("Iterator " + iterator + " has " + i + " elements, but expected " + values.size(), iterator.hasNext(), is(true)); + T obj = iterator.next(); + V val = property.apply(obj); + assertThat("For property value [" + val + "] of object [" + obj + "] at index [" + i + "]", val, equalTo(values.get(i))); + } + if (iterator.hasNext()) { + T obj = iterator.next(); + V val = property.apply(obj); + fail("Iterator " + iterator + " has more than [" + values.size() + "] elements" + + " - next element is [" + obj + "] with property value [" + val + "]"); + } + } + static class DummyRealm extends Realm { DummyRealm(String type, RealmConfig config) { @@ -625,5 +647,12 @@ public void authenticate(AuthenticationToken token, ActionListener listener) { listener.onResponse(null); } + + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + assertThat("Realms is null (but shouldn't be)", realms, notNullValue()); + assertThat("Realms returned null iterator (but shouldn't)", realms.iterator(), notNullValue()); + assertThat("Realms list does not contain this realm (but should)", realms, hasItem(this)); + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationActionTests.java new file mode 100644 index 0000000000000..b5eee48d2217d --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationActionTests.java @@ -0,0 +1,64 @@ +/* + * 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.xpack.security.rest.action; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; +import org.elasticsearch.license.License.OperationMode; +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.junit.Before; + +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class RestDelegatePkiAuthenticationActionTests extends ESTestCase { + + private long timeInMillis; + private Settings settings; + private TestUtils.UpdatableLicenseState licenseState; + private RestDelegatePkiAuthenticationAction action; + + @Before + public void init() { + timeInMillis = randomIntBetween(1, 1000); + settings = Settings.builder().put("xpack.security.enabled", true).build(); + licenseState = new TestUtils.UpdatableLicenseState(settings, () -> timeInMillis); + action = new RestDelegatePkiAuthenticationAction(settings, licenseState); + } + + public void testPkiAvailableOnGoldOrAbove() { + updateLicense(randomFrom(OperationMode.TRIAL, OperationMode.GOLD, OperationMode.PLATINUM, OperationMode.ENTERPRISE)); + assertNoRecordedFeatureUsage(); + assertThat(action.checkFeatureAvailable(new FakeRestRequest()), nullValue()); + assertFeatureUsageRecorded(); + } + + public void testPkiNotAvailableOnBasicOrStandard() { + updateLicense(randomFrom(OperationMode.BASIC, OperationMode.STANDARD)); + assertNoRecordedFeatureUsage(); + assertThat(action.checkFeatureAvailable(new FakeRestRequest()), throwableWithMessage(containsString("[pki]"))); + assertFeatureUsageRecorded(); + } + + protected void assertNoRecordedFeatureUsage() { + assertThat(licenseState.getLastUsed().get(XPackLicenseState.Feature.SECURITY_PKI_REALM), nullValue()); + } + + protected void assertFeatureUsageRecorded() { + assertThat(licenseState.getLastUsed().get(XPackLicenseState.Feature.SECURITY_PKI_REALM), is(timeInMillis)); + } + + private void updateLicense(License.OperationMode mode) { + this.licenseState.update(mode, true, null); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java index b7ab884ce42b8..a7bd4d65832b7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java @@ -11,40 +11,57 @@ import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.xpack.core.XPackSettings; -import org.hamcrest.Matchers; import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class SamlBaseRestHandlerTests extends ESTestCase { + private static final long FIXED_MILLIS = 99L; + private TestUtils.UpdatableLicenseState licenseState; + public void testSamlAvailableOnTrialAndPlatinum() { final SamlBaseRestHandler handler = buildHandler(randomFrom( License.OperationMode.TRIAL, License.OperationMode.PLATINUM, License.OperationMode.ENTERPRISE)); - assertThat(handler.checkFeatureAvailable(new FakeRestRequest()), Matchers.nullValue()); + assertNoRecordedFeatureUsage(); + assertThat(handler.checkFeatureAvailable(new FakeRestRequest()), nullValue()); + assertFeatureUsageRecorded(); } public void testSamlNotAvailableOnBasicStandardOrGold() { final SamlBaseRestHandler handler = buildHandler(randomFrom(License.OperationMode.BASIC, License.OperationMode.STANDARD, License.OperationMode.GOLD)); + assertNoRecordedFeatureUsage(); Exception e = handler.checkFeatureAvailable(new FakeRestRequest()); assertThat(e, instanceOf(ElasticsearchException.class)); ElasticsearchException elasticsearchException = (ElasticsearchException) e; assertThat(elasticsearchException.getMetadata(LicenseUtils.EXPIRED_FEATURE_METADATA), contains("saml")); + assertFeatureUsageRecorded(); + } + + protected void assertFeatureUsageRecorded() { + assertThat(licenseState.getLastUsed().get(XPackLicenseState.Feature.SECURITY_SAML_REALM), is(FIXED_MILLIS)); + } + + protected void assertNoRecordedFeatureUsage() { + assertThat(licenseState.getLastUsed().get(XPackLicenseState.Feature.SECURITY_SAML_REALM), nullValue()); } private SamlBaseRestHandler buildHandler(License.OperationMode licenseMode) { final Settings settings = Settings.builder() .put(XPackSettings.SECURITY_ENABLED.getKey(), true) .build(); - final TestUtils.UpdatableLicenseState licenseState = new TestUtils.UpdatableLicenseState(settings); + licenseState = new TestUtils.UpdatableLicenseState(settings, () -> FIXED_MILLIS); licenseState.update(licenseMode, true, null); return new SamlBaseRestHandler(settings, licenseState) {