From 05b52de9d8b0707820bac40b39094494cef51978 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 14 Oct 2021 18:32:14 +1100 Subject: [PATCH] Use a licensed feature per realm-type (+custom) (#79121) 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. Backport of: #78810 --- .../xpack/security/Security.java | 22 ++- .../xpack/security/authc/InternalRealms.java | 70 +++++--- .../xpack/security/authc/Realms.java | 49 +++--- .../RestDelegatePkiAuthenticationAction.java | 2 +- .../authc/AuthenticationServiceTests.java | 27 +++- .../security/authc/InternalRealmsTests.java | 50 ++++-- .../xpack/security/authc/RealmsTests.java | 153 +++++++++++------- 7 files changed, 257 insertions(+), 116 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 8c5cac73142be..e021e44c64c01 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 @@ -351,13 +351,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 d4a774570cae3..235c1a1b2e476 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; @@ -53,36 +57,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 = Sets.newHashSet(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 = org.elasticsearch.core.Map.ofEntries( + org.elasticsearch.core.Map.entry(AD_TYPE, Security.AD_REALM_FEATURE), + org.elasticsearch.core.Map.entry(LDAP_TYPE, Security.LDAP_REALM_FEATURE), + org.elasticsearch.core.Map.entry(PKI_TYPE, Security.PKI_REALM_FEATURE), + org.elasticsearch.core.Map.entry(SAML_TYPE, Security.SAML_REALM_FEATURE), + org.elasticsearch.core.Map.entry(KERBEROS_TYPE, Security.KERBEROS_REALM_FEATURE), + org.elasticsearch.core.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 org.elasticsearch.core.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 0ab835bbd5a91..1d906f777f4fa 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 @@ -17,7 +17,9 @@ import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.ThreadContext; 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.license.XPackLicenseState; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -127,18 +129,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 @@ -194,27 +203,29 @@ private boolean hasUserRealm(List licensedRealms) { } 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 e68be2d7fd77e..730d29af582f0 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 @@ -56,7 +56,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 f944fd85b1a7d..fdef9bd7f4db7 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; @@ -218,9 +219,14 @@ 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.isSecurityEnabled()).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); @@ -229,9 +235,10 @@ public void init() throws Exception { 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))); + realms = spy(new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), + Collections.emptyMap(), + licenseState, threadContext, reservedRealm, Arrays.asList(firstRealm, secondRealm), + Arrays.asList(firstRealm))); // Needed because this is calculated in the constructor, which means the override doesn't get called correctly realms.recomputeActiveRealms(); @@ -428,6 +435,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); } @@ -2171,7 +2179,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; @@ -2182,6 +2192,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 41c794358d6c2..6ed0288f80558 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,18 +10,16 @@ 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; import org.elasticsearch.xpack.core.security.authc.Realm; 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.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; @@ -31,9 +29,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; @@ -44,8 +44,14 @@ 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); @@ -60,11 +66,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 8bd3aedb51880..1a63e787c87a9 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<>(); @@ -109,20 +112,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); } @@ -155,8 +163,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); @@ -203,8 +210,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); @@ -253,10 +259,10 @@ public void testWithSettingsWhereDifferentRealmsHaveSameOrder() throws Exception 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); @@ -274,15 +280,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(); @@ -301,9 +314,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); @@ -337,7 +378,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(); @@ -398,7 +439,7 @@ 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() @@ -425,7 +466,7 @@ public void testUnlicensedWithInternalRealms() throws Exception { assertThat(types, contains("ldap", "type_0")); 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(); @@ -470,7 +511,7 @@ public void testUnlicensedWithInternalRealms() throws Exception { } public void testUnlicensedWithNativeRealmSettings() 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); Settings.Builder builder = Settings.builder() .put("path.home", createTempDir()) @@ -501,8 +542,8 @@ public void testUnlicensedWithNativeRealmSettings() 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(); @@ -521,9 +562,9 @@ public void testUnlicensedWithNativeRealmSettings() 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)); @@ -534,7 +575,8 @@ public void testUnlicensedWithNativeRealmSettings() 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()) @@ -552,8 +594,8 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realm.type(), is(selectedRealmType)); assertThat(iter.hasNext(), is(false)); 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(); @@ -573,10 +615,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(); @@ -596,11 +638,11 @@ 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 { @@ -864,8 +906,7 @@ public void testWarningsForImplicitlyDisabledBasicRealms() throws Exception { } 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 @@ -902,10 +943,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 void disableFileAndNativeRealms(Settings.Builder builder) { @@ -915,7 +960,7 @@ private void disableFileAndNativeRealms(Settings.Builder builder) { static class DummyRealm extends Realm { - DummyRealm(String type, RealmConfig config) { + DummyRealm(RealmConfig config) { super(config); }