From 327b8bda89476d822d050ae72946f27e76967ca7 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 13 Oct 2021 16:40:06 +1100 Subject: [PATCH] Use a licensed feature per realm-type (+custom) (#78810) This commit changes the licensed feature usage tracking for realms to record each realm type as its own separate feature. Custom realms continue to fall under a single catch-all feature. --- .../xpack/security/Security.java | 22 ++- .../xpack/security/authc/InternalRealms.java | 71 +++++-- .../xpack/security/authc/Realms.java | 56 +++--- .../RestDelegatePkiAuthenticationAction.java | 2 +- .../authc/AuthenticationServiceTests.java | 42 +++- .../security/authc/InternalRealmsTests.java | 56 ++++-- .../xpack/security/authc/RealmsTests.java | 181 +++++++++++------- 7 files changed, 296 insertions(+), 134 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 9619c96814558..619c55f5163d2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -356,13 +356,25 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, public static final LicensedFeature.Momentary AUDITING_FEATURE = LicensedFeature.momentaryLenient(null, "security_auditing", License.OperationMode.GOLD); + private static final String REALMS_FEATURE_FAMILY = "security-realms"; // Builtin realms (file/native) realms are Basic licensed, so don't need to be checked or tracked - // Standard realms (LDAP, AD, PKI, etc) are Gold+ + // Some realms (LDAP, AD, PKI) are Gold+ + public static final LicensedFeature.Persistent LDAP_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "ldap", License.OperationMode.GOLD); + public static final LicensedFeature.Persistent AD_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "active-directory", License.OperationMode.GOLD); + public static final LicensedFeature.Persistent PKI_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "pki", License.OperationMode.GOLD); // SSO realms are Platinum+ - public static final LicensedFeature.Persistent STANDARD_REALMS_FEATURE = - LicensedFeature.persistentLenient(null, "security_standard_realms", License.OperationMode.GOLD); - public static final LicensedFeature.Persistent ALL_REALMS_FEATURE = - LicensedFeature.persistentLenient(null, "security_all_realms", License.OperationMode.PLATINUM); + public static final LicensedFeature.Persistent SAML_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "saml", License.OperationMode.PLATINUM); + public static final LicensedFeature.Persistent OIDC_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "oidc", License.OperationMode.PLATINUM); + public static final LicensedFeature.Persistent KERBEROS_REALM_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "kerberos", License.OperationMode.PLATINUM); + // Custom realms are Platinum+ + public static final LicensedFeature.Persistent CUSTOM_REALMS_FEATURE = + LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "custom", License.OperationMode.PLATINUM); private static final Logger logger = LogManager.getLogger(Security.class); 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 cd37249698c15..f897355618f96 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 @@ -7,9 +7,12 @@ package org.elasticsearch.xpack.security.authc; import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -23,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.authc.esnative.NativeRealm; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; @@ -37,7 +41,6 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -52,36 +55,64 @@ */ public final class InternalRealms { + static final String RESERVED_TYPE = ReservedRealm.TYPE; + static final String NATIVE_TYPE = NativeRealmSettings.TYPE; + static final String FILE_TYPE = FileRealmSettings.TYPE; + static final String LDAP_TYPE = LdapRealmSettings.LDAP_TYPE; + static final String AD_TYPE = LdapRealmSettings.AD_TYPE; + static final String PKI_TYPE = PkiRealmSettings.TYPE; + static final String SAML_TYPE = SamlRealmSettings.TYPE; + static final String OIDC_TYPE = OpenIdConnectRealmSettings.TYPE; + static final String KERBEROS_TYPE = KerberosRealmSettings.TYPE; + + private static final Set BUILTIN_TYPES = Set.of(NATIVE_TYPE, FILE_TYPE); + /** - * The list of all internal realm types, excluding {@link ReservedRealm#TYPE}. + * The map of all licensed internal realm types to their licensed 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 LICENSED_REALMS = Map.ofEntries( + Map.entry(AD_TYPE, Security.AD_REALM_FEATURE), + Map.entry(LDAP_TYPE, Security.LDAP_REALM_FEATURE), + Map.entry(PKI_TYPE, Security.PKI_REALM_FEATURE), + Map.entry(SAML_TYPE, Security.SAML_REALM_FEATURE), + Map.entry(KERBEROS_TYPE, Security.KERBEROS_REALM_FEATURE), + Map.entry(OIDC_TYPE, Security.OIDC_REALM_FEATURE) + ); /** - * The list of all standard realm types, which are those provided by x-pack and do not have extensive - * interaction with third party sources + * The set of all internal realm types, excluding {@link ReservedRealm#TYPE} + * @deprecated Use of this method (other than in tests) is discouraged. */ - private static final Set STANDARD_TYPES = Collections.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, - FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE)); - + @Deprecated public static Collection getConfigurableRealmsTypes() { - return Collections.unmodifiableSet(XPACK_TYPES); + return Set.copyOf(Sets.union(BUILTIN_TYPES, LICENSED_REALMS.keySet())); } - /** - * Determines whether type is an internal realm-type that is provided by x-pack, - * excluding the {@link ReservedRealm} and realms that have extensive interaction with - * third party sources - */ - static boolean isStandardRealm(String type) { - return STANDARD_TYPES.contains(type); + static boolean isInternalRealm(String type) { + return RESERVED_TYPE.equals(type) || BUILTIN_TYPES.contains(type) || LICENSED_REALMS.containsKey(type); } static boolean isBuiltinRealm(String type) { - return FileRealmSettings.TYPE.equals(type) || NativeRealmSettings.TYPE.equals(type); + return BUILTIN_TYPES.contains(type); + } + + /** + * @return The licensed feature for the given realm type, or {@code null} if the realm does not require a specific license type + * @throws IllegalArgumentException if the provided type is not an {@link #isInternalRealm(String) internal realm} + */ + @Nullable + static LicensedFeature.Persistent getLicensedFeature(String type) { + if (Strings.isNullOrEmpty(type)) { + throw new IllegalArgumentException("Empty realm type [" + type + "]"); + } + if (type.equals(RESERVED_TYPE) || isBuiltinRealm(type)) { + return null; + } + final LicensedFeature.Persistent feature = LICENSED_REALMS.get(type); + if (feature == null) { + throw new IllegalArgumentException("Unsupported realm type [" + type + "]"); + } + return feature; } /** 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 53f167e807c03..8899dfa120d22 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 @@ -16,7 +16,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -76,9 +78,10 @@ public Realms(Settings settings, Environment env, Map fac assert factories.get(ReservedRealm.TYPE) == null; final List realmConfigs = buildRealmConfigs(); - this.allConfiguredRealms = initRealms(realmConfigs); - this.allConfiguredRealms.forEach(r -> r.initialize(allConfiguredRealms, licenseState)); - assert allConfiguredRealms.get(0) == reservedRealm : "the first realm must be reserved realm"; + final List initialRealms = initRealms(realmConfigs); + this.allConfiguredRealms = initialRealms; + this.allConfiguredRealms.forEach(r -> r.initialize(this.allConfiguredRealms, licenseState)); + assert this.allConfiguredRealms.get(0) == reservedRealm : "the first realm must be reserved realm"; recomputeActiveRealms(); licenseState.addListener(this::recomputeActiveRealms); @@ -93,18 +96,25 @@ protected void recomputeActiveRealms() { Strings.collectionToCommaDelimitedString(licensedRealms) ); + stopTrackingInactiveRealms(licenseStateSnapshot, licensedRealms); + + activeRealms = licensedRealms; + } + + // Can be overridden in testing + protected void stopTrackingInactiveRealms(XPackLicenseState licenseStateSnapshot, List licensedRealms) { // Stop license-tracking for any previously-active realms that are no longer allowed if (activeRealms != null) { activeRealms.stream().filter(r -> licensedRealms.contains(r) == false).forEach(realm -> { - if (InternalRealms.isStandardRealm(realm.type())) { - Security.STANDARD_REALMS_FEATURE.stopTracking(licenseStateSnapshot, realm.name()); - } else { - Security.ALL_REALMS_FEATURE.stopTracking(licenseStateSnapshot, realm.name()); - } + final LicensedFeature.Persistent feature = getLicensedFeatureForRealm(realm.type()); + assert feature != null : "Realm [" + + realm + + "] with no licensed feature became inactive due to change to license mode [" + + licenseStateSnapshot.getOperationMode() + + "]"; + feature.stopTracking(licenseStateSnapshot, realm.name()); }); } - - activeRealms = licensedRealms; } @Override @@ -142,27 +152,29 @@ protected List calculateLicensedRealms(XPackLicenseState licenseStateSnap } private static boolean checkLicense(Realm realm, XPackLicenseState licenseState) { - if (isBasicLicensedRealm(realm.type())) { + final LicensedFeature.Persistent feature = getLicensedFeatureForRealm(realm.type()); + if (feature == null) { return true; } - if (InternalRealms.isStandardRealm(realm.type())) { - return Security.STANDARD_REALMS_FEATURE.checkAndStartTracking(licenseState, realm.name()); - } - return Security.ALL_REALMS_FEATURE.checkAndStartTracking(licenseState, realm.name()); + return feature.checkAndStartTracking(licenseState, realm.name()); } public static boolean isRealmTypeAvailable(XPackLicenseState licenseState, String type) { - if (Security.ALL_REALMS_FEATURE.checkWithoutTracking(licenseState)) { + final LicensedFeature.Persistent feature = getLicensedFeatureForRealm(type); + if (feature == null) { return true; - } else if (Security.STANDARD_REALMS_FEATURE.checkWithoutTracking(licenseState)) { - return InternalRealms.isStandardRealm(type) || ReservedRealm.TYPE.equals(type); - } else { - return isBasicLicensedRealm(type); } + return feature.checkWithoutTracking(licenseState); } - private static boolean isBasicLicensedRealm(String type) { - return ReservedRealm.TYPE.equals(type) || InternalRealms.isBuiltinRealm(type); + @Nullable + private static LicensedFeature.Persistent getLicensedFeatureForRealm(String realmType) { + assert Strings.hasText(realmType) : "Realm type must be provided (received [" + realmType + "])"; + if (InternalRealms.isInternalRealm(realmType)) { + return InternalRealms.getLicensedFeature(realmType); + } else { + return Security.CUSTOM_REALMS_FEATURE; + } } public Realm realm(String name) { 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 855a95601de8c..2fc6e00bac939 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 @@ -55,7 +55,7 @@ protected Exception checkFeatureAvailable(RestRequest request) { Exception failedFeature = super.checkFeatureAvailable(request); if (failedFeature != null) { return failedFeature; - } else if (Security.STANDARD_REALMS_FEATURE.checkWithoutTracking(licenseState)) { + } else if (Security.PKI_REALM_FEATURE.checkWithoutTracking(licenseState)) { 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 ab97c81482d78..1b48f2ee8ee34 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 @@ -52,6 +52,7 @@ import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.license.License; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; @@ -79,6 +80,7 @@ import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.Realm.Factory; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; @@ -220,8 +222,13 @@ public void init() throws Exception { .put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true) .build(); MockLicenseState licenseState = mock(MockLicenseState.class); - when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(true); - when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(true); + for (String realmType : InternalRealms.getConfigurableRealmsTypes()) { + final LicensedFeature.Persistent feature = InternalRealms.getLicensedFeature(realmType); + if (feature != null) { + when(licenseState.isAllowed(feature)).thenReturn(true); + } + } + when(licenseState.isAllowed(Security.CUSTOM_REALMS_FEATURE)).thenReturn(true); when(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE)).thenReturn(true); when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); when(licenseState.checkFeature(Feature.SECURITY_AUDITING)).thenReturn(true); @@ -231,7 +238,7 @@ public void init() throws Exception { when(reservedRealm.type()).thenReturn("reserved"); when(reservedRealm.name()).thenReturn("reserved_realm"); realms = spy(new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), - Map.of(FileRealmSettings.TYPE, config -> mock(FileRealm.class), NativeRealmSettings.TYPE, config -> mock(NativeRealm.class)), + Map.of(FileRealmSettings.TYPE, this::mockRealm, NativeRealmSettings.TYPE, this::mockRealm), licenseState, threadContext, reservedRealm, Arrays.asList(firstRealm, secondRealm), Arrays.asList(firstRealm))); @@ -300,6 +307,25 @@ threadPool, new AnonymousUser(settings), tokenService, apiKeyService, serviceAcc operatorPrivilegesService); } + private Realm mockRealm(RealmConfig config) { + Class cls; + switch (config.type()) { + case InternalRealms.FILE_TYPE: + cls = FileRealm.class; + break; + case InternalRealms.NATIVE_TYPE: + cls = NativeRealm.class; + break; + default: + throw new IllegalArgumentException("No factory for realm " + config); + } + final Realm mock = mock(cls); + when(mock.type()).thenReturn(config.type()); + when(mock.name()).thenReturn(config.name()); + when(mock.order()).thenReturn(config.order()); + return mock; + } + @After public void shutdownThreadpool() throws InterruptedException { if (threadPool != null) { @@ -399,6 +425,7 @@ public void testAuthenticateBothSupportSecondSucceeds() throws Exception { verify(realms, atLeastOnce()).recomputeActiveRealms(); verify(realms, atLeastOnce()).calculateLicensedRealms(any(XPackLicenseState.class)); verify(realms, atLeastOnce()).getActiveRealms(); + verify(realms, atLeastOnce()).stopTrackingInactiveRealms(any(XPackLicenseState.class), any()); // ^^ We don't care how many times these methods are called, we just check it here so that we can verify no more interactions below. verifyNoMoreInteractions(realms); } @@ -2132,7 +2159,9 @@ protected List calculateLicensedRealms(XPackLicenseState licenseState) { // This can happen because the realms are recalculated during construction return super.calculateLicensedRealms(licenseState); } - if (Security.STANDARD_REALMS_FEATURE.checkWithoutTracking(licenseState)) { + + // Use custom as a placeholder for all non-internal realm + if (Security.CUSTOM_REALMS_FEATURE.checkWithoutTracking(licenseState)) { return allRealms; } else { return internalRealms; @@ -2143,6 +2172,11 @@ protected List calculateLicensedRealms(XPackLicenseState licenseState) { public void recomputeActiveRealms() { super.recomputeActiveRealms(); } + + @Override + protected void stopTrackingInactiveRealms(XPackLicenseState licenseStateSnapshot, List licensedRealms) { + // Ignore + } } private void logAndFail(Exception e) { 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 1d711287b2933..6509594848620 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 @@ -10,6 +10,8 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.License; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; @@ -17,12 +19,8 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; -import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; -import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.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.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; @@ -32,9 +30,11 @@ import java.util.function.BiConsumer; import static org.elasticsearch.mock.orig.Mockito.times; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -45,14 +45,22 @@ public class InternalRealmsTests extends ESTestCase { @SuppressWarnings("unchecked") public void testNativeRealmRegistersIndexHealthChangeListener() throws Exception { SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); - Map factories = InternalRealms.getFactories(mock(ThreadPool.class), mock(ResourceWatcherService.class), - mock(SSLService.class), mock(NativeUsersStore.class), mock(NativeRoleMappingStore.class), securityIndex); + Map factories = InternalRealms.getFactories( + mock(ThreadPool.class), + mock(ResourceWatcherService.class), + mock(SSLService.class), + mock(NativeUsersStore.class), + mock(NativeRoleMappingStore.class), + securityIndex + ); assertThat(factories, hasEntry(is(NativeRealmSettings.TYPE), any(Realm.Factory.class))); verifyZeroInteractions(securityIndex); final RealmConfig.RealmIdentifier realmId = new RealmConfig.RealmIdentifier(NativeRealmSettings.TYPE, "test"); - Settings settings = Settings.builder().put("path.home", createTempDir()) - .put(RealmSettings.getFullSettingKey(realmId, RealmSettings.ORDER_SETTING), 0).build(); + Settings settings = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.getFullSettingKey(realmId, RealmSettings.ORDER_SETTING), 0) + .build(); final Environment env = TestEnvironment.newEnvironment(settings); final ThreadContext threadContext = new ThreadContext(settings); factories.get(NativeRealmSettings.TYPE).create(new RealmConfig(realmId, settings, env, threadContext)); @@ -62,11 +70,31 @@ public void testNativeRealmRegistersIndexHealthChangeListener() throws Exception verify(securityIndex, times(2)).addStateListener(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 testLicenseLevels() { + for (String type : InternalRealms.getConfigurableRealmsTypes()) { + final LicensedFeature.Persistent feature = InternalRealms.getLicensedFeature(type); + if (InternalRealms.isBuiltinRealm(type)) { + assertThat(feature, nullValue()); + } else if (isStandardRealm(type)) { + assertThat(feature, notNullValue()); + // In theory a "standard" realm could actually be OperationMode.STANDARD, but we don't have any of those at the moment + assertThat(feature.getMinimumOperationMode(), is(License.OperationMode.GOLD)); + } else { + assertThat(feature, notNullValue()); + // In theory a (not-builtin & not-standard) realm could actually be OperationMode.ENTERPRISE, but we don't have any + assertThat(feature.getMinimumOperationMode(), is(License.OperationMode.PLATINUM)); + } + } + } + + private boolean isStandardRealm(String type) { + switch (type) { + case LdapRealmSettings.LDAP_TYPE: + case LdapRealmSettings.AD_TYPE: + case PkiRealmSettings.TYPE: + return true; + default: + return false; + } } } 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 fef7c037e1ef4..9221a95b368f8 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 @@ -16,6 +16,7 @@ import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseStateListener; +import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; @@ -27,6 +28,7 @@ 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.Security; @@ -46,6 +48,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -80,13 +83,13 @@ public class RealmsTests extends ESTestCase { @Before public void init() throws Exception { factories = new HashMap<>(); - factories.put(FileRealmSettings.TYPE, config -> new DummyRealm(FileRealmSettings.TYPE, config)); - factories.put(NativeRealmSettings.TYPE, config -> new DummyRealm(NativeRealmSettings.TYPE, config)); - factories.put(KerberosRealmSettings.TYPE, config -> new DummyRealm(KerberosRealmSettings.TYPE, config)); + factories.put(FileRealmSettings.TYPE, config -> new DummyRealm(config)); + factories.put(NativeRealmSettings.TYPE, config -> new DummyRealm(config)); + factories.put(KerberosRealmSettings.TYPE, config -> new DummyRealm(config)); randomRealmTypesCount = randomIntBetween(2, 5); for (int i = 0; i < randomRealmTypesCount; i++) { String name = "type_" + i; - factories.put(name, config -> new DummyRealm(name, config)); + factories.put(name, config -> new DummyRealm(config)); } licenseState = mock(MockLicenseState.class); licenseStateListeners = new ArrayList<>(); @@ -108,20 +111,25 @@ public void init() throws Exception { } private void allowAllRealms() { - when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(true); - when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(true); - licenseStateListeners.forEach(LicenseStateListener::licenseStateChanged); + setRealmAvailability(type -> true); } private void allowOnlyStandardRealms() { - when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(false); - when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(true); - licenseStateListeners.forEach(LicenseStateListener::licenseStateChanged); + setRealmAvailability(f -> f.getMinimumOperationMode() != License.OperationMode.PLATINUM); } private void allowOnlyNativeRealms() { - when(licenseState.isAllowed(Security.ALL_REALMS_FEATURE)).thenReturn(false); - when(licenseState.isAllowed(Security.STANDARD_REALMS_FEATURE)).thenReturn(false); + setRealmAvailability(type -> false); + } + + private void setRealmAvailability(Function body) { + InternalRealms.getConfigurableRealmsTypes().forEach(type -> { + final LicensedFeature.Persistent feature = InternalRealms.getLicensedFeature(type); + if (feature != null) { + when(licenseState.isAllowed(feature)).thenReturn(body.apply(feature)); + } + }); + when(licenseState.isAllowed(Security.CUSTOM_REALMS_FEATURE)).thenReturn(body.apply(Security.CUSTOM_REALMS_FEATURE)); licenseStateListeners.forEach(LicenseStateListener::licenseStateChanged); } @@ -154,8 +162,7 @@ public void testRealmTypeAvailable() { } public void testWithSettings() throws Exception { - Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); List orders = new ArrayList<>(randomRealmTypesCount); for (int i = 0; i < randomRealmTypesCount; i++) { orders.add(i); @@ -177,9 +184,9 @@ public void testWithSettings() throws Exception { verify(licenseState, times(1)).getOperationMode(); // Verify that we recorded licensed-feature use for each realm (this is trigger on license load during node startup) - verify(licenseState, Mockito.atLeast(randomRealmTypesCount)).isAllowed(Security.ALL_REALMS_FEATURE); + verify(licenseState, Mockito.atLeast(randomRealmTypesCount)).isAllowed(Security.CUSTOM_REALMS_FEATURE); for (int i = 0; i < randomRealmTypesCount; i++) { - verify(licenseState, atLeastOnce()).enableUsageTracking(Security.ALL_REALMS_FEATURE, "realm_" + i); + verify(licenseState, atLeastOnce()).enableUsageTracking(Security.CUSTOM_REALMS_FEATURE, "realm_" + i); } verifyNoMoreInteractions(licenseState); @@ -207,8 +214,7 @@ public void testWithSettings() throws Exception { } public void testWithSettingsWhereDifferentRealmsHaveSameOrder() throws Exception { - Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); List randomSeq = new ArrayList<>(randomRealmTypesCount); for (int i = 0; i < randomRealmTypesCount; i++) { randomSeq.add(i); @@ -225,18 +231,19 @@ public void testWithSettingsWhereDifferentRealmsHaveSameOrder() throws Exception } Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->{ - new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); - }); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> { new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); } + ); assertThat(e.getMessage(), containsString("Found multiple realms configured with the same order")); } public void testWithSettingsWithMultipleInternalRealmsOfSameType() throws Exception { Settings settings = Settings.builder() - .put("xpack.security.authc.realms.file.realm_1.order", 0) - .put("xpack.security.authc.realms.file.realm_2.order", 1) - .put("path.home", createTempDir()) - .build(); + .put("xpack.security.authc.realms.file.realm_1.order", 0) + .put("xpack.security.authc.realms.file.realm_2.order", 1) + .put("path.home", createTempDir()) + .build(); Environment env = TestEnvironment.newEnvironment(settings); try { new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); @@ -254,15 +261,22 @@ public void testWithSettingsWithMultipleRealmsWithSameName() throws Exception { .put("path.home", createTempDir()) .build(); Environment env = TestEnvironment.newEnvironment(settings); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->{ - new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); - }); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> { new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); } + ); assertThat(e.getMessage(), containsString("Found multiple realms configured with the same name")); } public void testWithEmptySettings() throws Exception { - Realms realms = new Realms(Settings.EMPTY, TestEnvironment.newEnvironment(Settings.builder().put("path.home", - createTempDir()).build()), factories, licenseState, threadContext, reservedRealm); + Realms realms = new Realms( + Settings.EMPTY, + TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build()), + factories, + licenseState, + threadContext, + reservedRealm + ); Iterator iter = realms.iterator(); assertThat(iter.hasNext(), is(true)); Realm realm = iter.next(); @@ -281,9 +295,37 @@ public void testWithEmptySettings() throws Exception { assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); } + public void testFeatureTrackingWithMultipleRealms() throws Exception { + factories.put(LdapRealmSettings.LDAP_TYPE, DummyRealm::new); + factories.put(PkiRealmSettings.TYPE, DummyRealm::new); + + Settings settings = Settings.builder() + .put("xpack.security.authc.realms.file.file_realm.order", 0) + .put("xpack.security.authc.realms.native.native_realm.order", 1) + .put("xpack.security.authc.realms.kerberos.kerberos_realm.order", 2) + .put("xpack.security.authc.realms.ldap.ldap_realm_1.order", 3) + .put("xpack.security.authc.realms.ldap.ldap_realm_2.order", 4) + .put("xpack.security.authc.realms.pki.pki_realm.order", 5) + .put("xpack.security.authc.realms.type_0.custom_realm_1.order", 6) + .put("xpack.security.authc.realms.type_1.custom_realm_2.order", 7) + .put("path.home", createTempDir()) + .build(); + Environment env = TestEnvironment.newEnvironment(settings); + + Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); + assertThat(realms.getUnlicensedRealms(), empty()); + assertThat(realms.getActiveRealms(), hasSize(9)); // 0..7 configured + reserved + + verify(licenseState).enableUsageTracking(Security.KERBEROS_REALM_FEATURE, "kerberos_realm"); + verify(licenseState).enableUsageTracking(Security.LDAP_REALM_FEATURE, "ldap_realm_1"); + verify(licenseState).enableUsageTracking(Security.LDAP_REALM_FEATURE, "ldap_realm_2"); + verify(licenseState).enableUsageTracking(Security.PKI_REALM_FEATURE, "pki_realm"); + verify(licenseState).enableUsageTracking(Security.CUSTOM_REALMS_FEATURE, "custom_realm_1"); + verify(licenseState).enableUsageTracking(Security.CUSTOM_REALMS_FEATURE, "custom_realm_2"); + } + public void testUnlicensedWithOnlyCustomRealms() throws Exception { - Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); List orders = new ArrayList<>(randomRealmTypesCount); for (int i = 0; i < randomRealmTypesCount; i++) { orders.add(i); @@ -323,7 +365,7 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), empty()); assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); for (i = 0; i < randomRealmTypesCount; i++) { - verify(licenseState).enableUsageTracking(Security.ALL_REALMS_FEATURE, "realm_" + i); + verify(licenseState).enableUsageTracking(Security.CUSTOM_REALMS_FEATURE, "realm_" + i); } allowOnlyNativeRealms(); @@ -348,13 +390,13 @@ public void testUnlicensedWithOnlyCustomRealms() throws Exception { } public void testUnlicensedWithInternalRealms() throws Exception { - factories.put(LdapRealmSettings.LDAP_TYPE, config -> new DummyRealm(LdapRealmSettings.LDAP_TYPE, config)); + factories.put(LdapRealmSettings.LDAP_TYPE, config -> new DummyRealm(config)); assertThat(factories.get("type_0"), notNullValue()); String ldapRealmName = randomAlphaOfLengthBetween(3, 8); Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()) - .put("xpack.security.authc.realms.ldap." + ldapRealmName + ".order", "0") - .put("xpack.security.authc.realms.type_0.custom.order", "1"); + .put("path.home", createTempDir()) + .put("xpack.security.authc.realms.ldap." + ldapRealmName + ".order", "0") + .put("xpack.security.authc.realms.type_0.custom.order", "1"); final boolean fileRealmDisabled = randomDisableRealm(builder, FileRealmSettings.TYPE); final boolean nativeRealmDisabled = randomDisableRealm(builder, NativeRealmSettings.TYPE); Settings settings = builder.build(); @@ -374,7 +416,7 @@ public void testUnlicensedWithInternalRealms() throws Exception { assertThat(realms.getUnlicensedRealms(), empty()); assertThat(realms.getUnlicensedRealms(), sameInstance(realms.getUnlicensedRealms())); - verify(licenseState).enableUsageTracking(Security.STANDARD_REALMS_FEATURE, ldapRealmName); + verify(licenseState).enableUsageTracking(Security.LDAP_REALM_FEATURE, ldapRealmName); allowOnlyStandardRealms(); iter = realms.iterator(); @@ -408,13 +450,13 @@ public void testUnlicensedWithInternalRealms() throws Exception { } public void testUnlicensedWithBasicRealmSettings() throws Exception { - factories.put(LdapRealmSettings.LDAP_TYPE, config -> new DummyRealm(LdapRealmSettings.LDAP_TYPE, config)); + factories.put(LdapRealmSettings.LDAP_TYPE, config -> new DummyRealm(config)); final String type = randomFrom(FileRealmSettings.TYPE, NativeRealmSettings.TYPE); final String otherType = FileRealmSettings.TYPE.equals(type) ? NativeRealmSettings.TYPE : FileRealmSettings.TYPE; Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()) - .put("xpack.security.authc.realms.ldap.foo.order", "0") - .put("xpack.security.authc.realms." + type + ".native.order", "1"); + .put("path.home", createTempDir()) + .put("xpack.security.authc.realms.ldap.foo.order", "0") + .put("xpack.security.authc.realms." + type + ".native.order", "1"); final boolean otherTypeDisabled = randomDisableRealm(builder, otherType); Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); @@ -444,8 +486,8 @@ public void testUnlicensedWithBasicRealmSettings() throws Exception { verify(licenseState, times(1)).getOperationMode(); // Verify that we recorded licensed-feature use for each licensed realm (this is trigger on license load/change) - verify(licenseState, times(1)).isAllowed(Security.STANDARD_REALMS_FEATURE); - verify(licenseState).enableUsageTracking(Security.STANDARD_REALMS_FEATURE, "foo"); + verify(licenseState, times(1)).isAllowed(Security.LDAP_REALM_FEATURE); + verify(licenseState).enableUsageTracking(Security.LDAP_REALM_FEATURE, "foo"); verifyNoMoreInteractions(licenseState); allowOnlyNativeRealms(); @@ -468,9 +510,9 @@ public void testUnlicensedWithBasicRealmSettings() throws Exception { assertThat(iter.hasNext(), is(false)); // Verify that we checked (a 2nd time) the license for the non-basic realm - verify(licenseState, times(2)).isAllowed(Security.STANDARD_REALMS_FEATURE); - // Verify that we stopped tracking use for realms which are no longer licensed - verify(licenseState).disableUsageTracking(Security.STANDARD_REALMS_FEATURE, "foo"); + verify(licenseState, times(2)).isAllowed(Security.LDAP_REALM_FEATURE); + // Verify that we stopped tracking use for realms which are no longer licensed + verify(licenseState).disableUsageTracking(Security.LDAP_REALM_FEATURE, "foo"); verifyNoMoreInteractions(licenseState); assertThat(realms.getUnlicensedRealms(), iterableWithSize(1)); @@ -481,7 +523,8 @@ public void testUnlicensedWithBasicRealmSettings() throws Exception { public void testUnlicensedWithNonStandardRealms() throws Exception { final String selectedRealmType = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE, OpenIdConnectRealmSettings.TYPE); - factories.put(selectedRealmType, config -> new DummyRealm(selectedRealmType, config)); + factories.put(selectedRealmType, config -> new DummyRealm(config)); + final LicensedFeature.Persistent feature = InternalRealms.getLicensedFeature(selectedRealmType); String realmName = randomAlphaOfLengthBetween(3, 8); Settings.Builder builder = Settings.builder() .put("path.home", createTempDir()) @@ -500,8 +543,8 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { realm = iter.next(); assertThat(realm.type(), is(selectedRealmType)); assertThat(realms.getUnlicensedRealms(), empty()); - verify(licenseState, times(1)).isAllowed(Security.ALL_REALMS_FEATURE); - verify(licenseState, times(1)).enableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); + verify(licenseState, times(1)).isAllowed(feature); + verify(licenseState, times(1)).enableUsageTracking(feature, realmName); allowOnlyStandardRealms(); iter = realms.iterator(); @@ -515,10 +558,10 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realm.type(), equalTo(selectedRealmType)); assertThat(realm.name(), equalTo(realmName)); - verify(licenseState, times(2)).isAllowed(Security.ALL_REALMS_FEATURE); - verify(licenseState, times(1)).disableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); + verify(licenseState, times(2)).isAllowed(feature); + verify(licenseState, times(1)).disableUsageTracking(feature, realmName); // this happened when the realm was allowed. Check it's still only 1 call - verify(licenseState, times(1)).enableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); + verify(licenseState, times(1)).enableUsageTracking(feature, realmName); allowOnlyNativeRealms(); iter = realms.iterator(); @@ -532,16 +575,15 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realm.type(), equalTo(selectedRealmType)); assertThat(realm.name(), equalTo(realmName)); - verify(licenseState, times(3)).isAllowed(Security.ALL_REALMS_FEATURE); + verify(licenseState, times(3)).isAllowed(feature); // this doesn't get called a second time because it didn't change - verify(licenseState, times(1)).disableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); + verify(licenseState, times(1)).disableUsageTracking(feature, realmName); // this happened when the realm was allowed. Check it's still only 1 call - verify(licenseState, times(1)).enableUsageTracking(Security.ALL_REALMS_FEATURE, realmName); + verify(licenseState, times(1)).enableUsageTracking(feature, realmName); } public void testDisabledRealmsAreNotAdded() throws Exception { - Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); List orders = new ArrayList<>(randomRealmTypesCount); for (int i = 0; i < randomRealmTypesCount; i++) { orders.add(i); @@ -589,9 +631,9 @@ public void testDisabledRealmsAreNotAdded() throws Exception { public void testUsageStats() throws Exception { // test realms with duplicate values Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()) - .put("xpack.security.authc.realms.type_0.foo.order", "0") - .put("xpack.security.authc.realms.type_0.bar.order", "1"); + .put("path.home", createTempDir()) + .put("xpack.security.authc.realms.type_0.foo.order", "0") + .put("xpack.security.authc.realms.type_0.bar.order", "1"); final boolean fileRealmDisabled = randomDisableRealm(builder, FileRealmSettings.TYPE); final boolean nativeRealmDisabled = randomDisableRealm(builder, NativeRealmSettings.TYPE); Settings settings = builder.build(); @@ -677,8 +719,7 @@ public void testInitRealmsFailsForMultipleKerberosRealms() throws IOException { } public void testWarningsForReservedPrefixedRealmNames() throws Exception { - Settings.Builder builder = Settings.builder() - .put("path.home", createTempDir()); + Settings.Builder builder = Settings.builder().put("path.home", createTempDir()); final boolean invalidFileRealmName = randomBoolean(); final boolean invalidNativeRealmName = randomBoolean(); // Ensure at least one realm has invalid name @@ -715,10 +756,14 @@ public void testWarningsForReservedPrefixedRealmNames() throws Exception { Environment env = TestEnvironment.newEnvironment(settings); new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); - assertWarnings("Found realm " + (invalidRealmNames.size() == 1 ? "name" : "names") - + " with reserved prefix [_]: [" - + Strings.collectionToDelimitedString(invalidRealmNames.stream().sorted().collect(Collectors.toList()), "; ") + "]. " - + "In a future major release, node will fail to start if any realm names start with reserved prefix."); + assertWarnings( + "Found realm " + + (invalidRealmNames.size() == 1 ? "name" : "names") + + " with reserved prefix [_]: [" + + Strings.collectionToDelimitedString(invalidRealmNames.stream().sorted().collect(Collectors.toList()), "; ") + + "]. " + + "In a future major release, node will fail to start if any realm names start with reserved prefix." + ); } private boolean randomDisableRealm(Settings.Builder builder, String type) { @@ -755,7 +800,7 @@ private void assertImplicitlyAddedBasicRealms(Iterator iter, boolean file static class DummyRealm extends Realm { - DummyRealm(String type, RealmConfig config) { + DummyRealm(RealmConfig config) { super(config); }