From 731a1f2f08bfa6c98c66a373f148572a8a823d20 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 4 Sep 2020 17:30:51 +1000 Subject: [PATCH 1/3] Enable license feature usage for Security Realms Changes the way that license checking for realms is performed so that it is compatible with feature usage reporting. The detailed changes are: 1. Replace the old license features of "ALL_REALMS" and "STANDARD_REALMS" with a new feature per realm type (+"CUSTOM") 2. When evaluating realms against the license, check the configured realms against their coresponding feature. We know only perform checks for realms that have been configured 3. Switched the previous license check that was performed in Realms.asList() to a new filtering process in a license state listener. This was needed due to the increased number of feature types that we were checking against. Because these checks are done on license change, the "last used" in the feature usage reflects the more recent of "node startup" or "license updated". This is not strictly accurate, since a realm might (and typically will) be used more frequently than that to perform authentications. However, for SAML, OIDC, and Delegated PKI the authentication flow uses specific Rest actions that do explicit license checking (primarily to provide client and explicit error messages) and these will report an accurate "last used" time. For LDAP, AD, PKI, Kerberos and Custom realms, we might consider a followup change to update the last-used time each time authenticated is successful. --- .../license/XPackLicenseState.java | 16 +- .../org/elasticsearch/license/TestUtils.java | 7 +- .../license/XPackLicenseStateTests.java | 42 ++++- .../xpack/security/authc/InternalRealms.java | 85 ++++++---- .../xpack/security/authc/Realms.java | 134 ++++++++------- .../RestDelegatePkiAuthenticationAction.java | 4 +- .../authc/AuthenticationServiceTests.java | 49 ++++-- .../security/authc/InternalRealmsTests.java | 22 ++- .../xpack/security/authc/RealmsTests.java | 155 +++++++++--------- ...tDelegatePkiAuthenticationActionTests.java | 64 ++++++++ .../action/saml/SamlBaseRestHandlerTests.java | 23 ++- 11 files changed, 384 insertions(+), 217 deletions(-) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationActionTests.java 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..ea88432641437 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), @@ -54,6 +59,7 @@ public enum Feature { SECURITY_AUTHORIZATION_ENGINE(OperationMode.PLATINUM, true), SECURITY_STATS_AND_HEALTH(OperationMode.MISSING, true), + WATCHER(OperationMode.STANDARD, true), MONITORING(OperationMode.MISSING, true), // TODO: should just check WATCHER directly? @@ -109,12 +115,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..120476278b4e1 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 native realms to enable if no other realms are permitted under the current license + private final List fallbackNativeRealms; + + // 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,45 @@ 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.allConfiguredRealms.forEach(r -> r.initialize(this, licenseState)); + this.activeRealms = allConfiguredRealms; - for (List realmList : Arrays.asList(standardRealms, nativeRealms)) { - if (realmList.isEmpty()) { - addNativeRealms(realmList); - } + this.fallbackNativeRealms = buildFallbackRealms(reservedRealm); + + licenseState.addListener(this::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 = fallbackNativeRealms; + } + 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 +118,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 +134,14 @@ 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; - } + return activeRealms; } public Realm realm(String name) { - for (Realm realm : realms) { + for (Realm realm : activeRealms) { if (name.equals(realm.name())) { return realm; } @@ -170,7 +160,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 +177,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 +217,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 +286,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 +303,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 +344,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..15bf15ee5588b 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,29 @@ 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 (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..dde9be1f07e0e 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,10 @@ 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.Matchers.arrayWithSize; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; @@ -49,7 +57,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 +68,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 +81,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 +103,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 +262,10 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { Environment env = TestEnvironment.newEnvironment(settings); Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); + allowAllRealms(); + 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 +285,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 +378,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 +463,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 +500,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 +514,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 +586,27 @@ public void testInitRealmsFailsForMultipleKerberosRealms() throws IOException { "multiple realms [realm_1, realm_2] configured of type [kerberos], [kerberos] can only have one such realm configured"))); } + 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) { 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) { From 9086265a0b257903a377ef443e341c66066c3696 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 5 Oct 2020 17:03:34 +1100 Subject: [PATCH 2/3] Address feedback --- .../org/elasticsearch/license/XPackLicenseState.java | 1 - .../org/elasticsearch/xpack/security/authc/Realms.java | 9 +++++---- .../xpack/security/authc/AuthenticationServiceTests.java | 4 ++++ .../elasticsearch/xpack/security/authc/RealmsTests.java | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) 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 ea88432641437..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 @@ -59,7 +59,6 @@ public enum Feature { SECURITY_AUTHORIZATION_ENGINE(OperationMode.PLATINUM, true), SECURITY_STATS_AND_HEALTH(OperationMode.MISSING, true), - WATCHER(OperationMode.STANDARD, true), MONITORING(OperationMode.MISSING, true), // TODO: should just check WATCHER directly? 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 120476278b4e1..5b15fe4dac10a 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 @@ -53,8 +53,8 @@ public class Realms implements Iterable { // 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 native realms to enable if no other realms are permitted under the current license - private final List fallbackNativeRealms; + // 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; @@ -73,9 +73,10 @@ public Realms(Settings settings, Environment env, Map fac this.allConfiguredRealms.forEach(r -> r.initialize(this, licenseState)); this.activeRealms = allConfiguredRealms; - this.fallbackNativeRealms = buildFallbackRealms(reservedRealm); + this.fallbackRealms = buildFallbackRealms(reservedRealm); licenseState.addListener(this::recomputeActiveRealms); + recomputeActiveRealms(); } protected void recomputeActiveRealms() { @@ -84,7 +85,7 @@ protected void recomputeActiveRealms() { if (hasConfigurableRealms(licensedRealms)) { activeRealms = licensedRealms; } else { - activeRealms = fallbackNativeRealms; + activeRealms = fallbackRealms; } if (logger.isDebugEnabled()) { logger.debug("license state has changed to [{}]; configured realms are: [{}]; active realms are: [{}]", 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 15bf15ee5588b..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 @@ -1609,6 +1609,10 @@ static class TestRealms extends Realms { @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 { 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 dde9be1f07e0e..386ff696a752d 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 @@ -263,6 +263,7 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { 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); From f52ec4315d024d5298487c639dca97b822876acd Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 6 Oct 2020 12:00:00 +1100 Subject: [PATCH 3/3] Fix delegated authoriztion --- .../xpack/security/authc/Realms.java | 6 ++++-- .../xpack/security/authc/RealmsTests.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) 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 5b15fe4dac10a..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 @@ -70,10 +70,11 @@ public Realms(Settings settings, Environment env, Map fac assert factories.get(ReservedRealm.TYPE) == null; this.allConfiguredRealms = initRealms(); - this.allConfiguredRealms.forEach(r -> r.initialize(this, licenseState)); + this.fallbackRealms = buildFallbackRealms(reservedRealm); this.activeRealms = allConfiguredRealms; - this.fallbackRealms = buildFallbackRealms(reservedRealm); + 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(); @@ -138,6 +139,7 @@ public List asList() { if (licenseState.isSecurityEnabled() == false) { return Collections.emptyList(); } + assert activeRealms != null : "Active realms not configured"; return activeRealms; } 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 386ff696a752d..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 @@ -45,6 +45,7 @@ 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; @@ -587,6 +588,19 @@ 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(",")), @@ -633,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)); + } } }