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 f48e4209ac61e..36c0ecfbd775f 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 @@ -42,7 +42,6 @@ public class XPackLicenseState { */ public enum Feature { SECURITY_AUDITING(OperationMode.GOLD, false), - SECURITY_CUSTOM_ROLE_PROVIDERS(OperationMode.PLATINUM, true), SECURITY_TOKEN_SERVICE(OperationMode.STANDARD, false), SECURITY_AUTHORIZATION_REALM(OperationMode.PLATINUM, true), SECURITY_AUTHORIZATION_ENGINE(OperationMode.PLATINUM, true), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java index 0823f06eee0ef..03a94afa321b9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/MockLicenseState.java @@ -7,8 +7,13 @@ package org.elasticsearch.license; +import org.mockito.Mockito; + +import java.util.function.Consumer; import java.util.function.LongSupplier; +import static org.mockito.Mockito.doAnswer; + /** A license state that may be mocked by testing because the internal methods are made public */ public class MockLicenseState extends XPackLicenseState { @@ -30,4 +35,18 @@ public void enableUsageTracking(LicensedFeature feature, String contextName) { public void disableUsageTracking(LicensedFeature feature, String contextName) { super.disableUsageTracking(feature, contextName); } + + public static MockLicenseState createMock() { + MockLicenseState mock = Mockito.mock(MockLicenseState.class); + Mockito.when(mock.copyCurrentLicenseState()).thenReturn(mock); + return mock; + } + + public static void acceptListeners(MockLicenseState licenseState, Consumer addListener) { + doAnswer(inv -> { + final LicenseStateListener listener = (LicenseStateListener) inv.getArguments()[0]; + addListener.accept(listener); + return null; + }).when(licenseState).addListener(Mockito.any()); + } } 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 3352f600da5d2..32f0f36aa8c4b 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 @@ -89,7 +89,6 @@ public static OperationMode randomBasicStandardOrGold() { public void testSecurityDefaults() { XPackLicenseState licenseState = new XPackLicenseState(() -> 0); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(true)); } public void testSecurityStandard() { @@ -97,7 +96,6 @@ public void testSecurityStandard() { licenseState.update(STANDARD, true, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(false)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } @@ -106,7 +104,6 @@ public void testSecurityStandardExpired() { licenseState.update(STANDARD, false, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(false)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } @@ -115,7 +112,6 @@ public void testSecurityBasic() { licenseState.update(BASIC, true, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(false)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(false)); } @@ -124,7 +120,6 @@ public void testSecurityGold() { licenseState.update(GOLD, true, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } @@ -133,7 +128,6 @@ public void testSecurityGoldExpired() { licenseState.update(GOLD, false, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } @@ -142,7 +136,6 @@ public void testSecurityPlatinum() { licenseState.update(PLATINUM, true, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(true)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } @@ -151,7 +144,6 @@ public void testSecurityPlatinumExpired() { licenseState.update(PLATINUM, false, null); assertThat(licenseState.checkFeature(Feature.SECURITY_AUDITING), is(true)); - assertThat(licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS), is(false)); assertThat(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE), is(true)); } 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 bc1c62efbaddd..2ca883b32c662 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 @@ -245,6 +245,7 @@ import org.elasticsearch.xpack.security.authz.store.FileRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; +import org.elasticsearch.xpack.security.authz.store.RoleProviders; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; import org.elasticsearch.xpack.security.operator.FileOperatorUsersStore; import org.elasticsearch.xpack.security.operator.OperatorOnlyRegistry; @@ -318,6 +319,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -376,6 +378,10 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, public static final LicensedFeature.Persistent CUSTOM_REALMS_FEATURE = LicensedFeature.persistentLenient(REALMS_FEATURE_FAMILY, "custom", License.OperationMode.PLATINUM); + // Custom role providers are Platinum+ + public static final LicensedFeature.Persistent CUSTOM_ROLE_PROVIDERS_FEATURE = + LicensedFeature.persistent(null, "security-roles-provider", License.OperationMode.PLATINUM); + private static final Logger logger = LogManager.getLogger(Security.class); public static final SystemIndexDescriptor SECURITY_MAIN_INDEX_DESCRIPTOR = getSecurityMainIndexDescriptor(); @@ -423,7 +429,6 @@ public Security(Settings settings, final Path configPath) { this.bootstrapChecks.set(Collections.emptyList()); } this.securityExtensions.addAll(extensions); - } private static void runStartupChecks(Settings settings) { @@ -548,9 +553,15 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste xContentRegistry); final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, getLicenseState(), securityIndex.get()); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); - List, ActionListener>> rolesProviders = new ArrayList<>(); + + final Map, ActionListener>>> customRoleProviders = new LinkedHashMap<>(); for (SecurityExtension extension : securityExtensions) { - rolesProviders.addAll(extension.getRolesProviders(extensionComponents)); + final List, ActionListener>> providers = extension.getRolesProviders( + extensionComponents + ); + if (providers != null && providers.isEmpty() == false) { + customRoleProviders.put(extension.toString(), providers); + } } final ApiKeyService apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, securityIndex.get(), @@ -572,8 +583,15 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste ); components.add(serviceAccountService); - final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, - privilegeStore, rolesProviders, threadPool.getThreadContext(), getLicenseState(), fieldPermissionsCache, apiKeyService, + final RoleProviders roleProviders = new RoleProviders( + reservedRolesStore, + fileRolesStore, + nativeRolesStore, + customRoleProviders, + getLicenseState() + ); + final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, roleProviders, + privilegeStore, threadPool.getThreadContext(), getLicenseState(), fieldPermissionsCache, apiKeyService, serviceAccountService, dlsBitsetCache.get(), expressionResolver, new DeprecationRoleDescriptorConsumer(clusterService, threadPool)); securityIndex.get().addStateListener(allRolesStore::onSecurityIndexStateChange); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index d770fc698106c..5477fe77e0d9d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -30,7 +30,6 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.license.XPackLicenseState.Feature; import org.elasticsearch.xpack.core.common.IteratingActionListener; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -84,8 +83,8 @@ import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isMoveFromRedToNonRed; /** - * A composite roles store that combines built in roles, file-based roles, and index-based roles. Checks the built in roles first, then the - * file roles, and finally the index roles. + * A composite roles store that can retrieve roles from multiple sources. + * @see RoleProviders */ public class CompositeRolesStore { @@ -98,8 +97,7 @@ public class CompositeRolesStore { private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(CompositeRolesStore.class); - private final FileRolesStore fileRolesStore; - private final NativeRolesStore nativeRolesStore; + private final RoleProviders roleProviders; private final NativePrivilegeStore privilegeStore; private final XPackLicenseState licenseState; private final Consumer> effectiveRoleDescriptorsConsumer; @@ -114,25 +112,31 @@ public class CompositeRolesStore { private final ApiKeyService apiKeyService; private final ServiceAccountService serviceAccountService; private final boolean isAnonymousEnabled; - private final List, ActionListener>> builtInRoleProviders; - private final List, ActionListener>> allRoleProviders; private final Role superuserRole; private final Role xpackUserRole; private final Role asyncSearchUserRole; private final Automaton restrictedIndicesAutomaton; - public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore, - ReservedRolesStore reservedRolesStore, NativePrivilegeStore privilegeStore, - List, ActionListener>> rolesProviders, + public CompositeRolesStore(Settings settings, RoleProviders roleProviders, NativePrivilegeStore privilegeStore, ThreadContext threadContext, XPackLicenseState licenseState, FieldPermissionsCache fieldPermissionsCache, ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, DocumentSubsetBitsetCache dlsBitsetCache, IndexNameExpressionResolver resolver, Consumer> effectiveRoleDescriptorsConsumer) { - this.fileRolesStore = Objects.requireNonNull(fileRolesStore); - this.dlsBitsetCache = Objects.requireNonNull(dlsBitsetCache); - fileRolesStore.addListener(this::invalidate); - this.nativeRolesStore = Objects.requireNonNull(nativeRolesStore); + this.roleProviders = roleProviders; + roleProviders.addChangeListener(new RoleProviders.ChangeListener() { + @Override + public void rolesChanged(Set roles) { + CompositeRolesStore.this.invalidate(roles); + } + + @Override + public void providersChanged() { + CompositeRolesStore.this.invalidateAll(); + } + }); + this.privilegeStore = Objects.requireNonNull(privilegeStore); + this.dlsBitsetCache = Objects.requireNonNull(dlsBitsetCache); this.licenseState = Objects.requireNonNull(licenseState); this.fieldPermissionsCache = Objects.requireNonNull(fieldPermissionsCache); this.apiKeyService = Objects.requireNonNull(apiKeyService); @@ -152,16 +156,6 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat nlcBuilder.setMaximumWeight(nlcCacheSize); } this.negativeLookupCache = nlcBuilder.build(); - this.builtInRoleProviders = List.of(reservedRolesStore, fileRolesStore, nativeRolesStore); - if (rolesProviders.isEmpty()) { - this.allRoleProviders = this.builtInRoleProviders; - } else { - List, ActionListener>> allList = - new ArrayList<>(builtInRoleProviders.size() + rolesProviders.size()); - allList.addAll(builtInRoleProviders); - allList.addAll(rolesProviders); - this.allRoleProviders = Collections.unmodifiableList(allList); - } this.anonymousUser = new AnonymousUser(settings); this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.restrictedIndicesAutomaton = resolver.getSystemNameAutomaton(); @@ -411,9 +405,7 @@ private void roleDescriptors(Set roleNames, ActionListener roleNames, ActionListener listener) { final RolesRetrievalResult rolesResult = new RolesRetrievalResult(); - final List, ActionListener>> asyncRoleProviders = - licenseState.checkFeature(Feature.SECURITY_CUSTOM_ROLE_PROVIDERS) ? allRoleProviders : builtInRoleProviders; - + final List, ActionListener>> asyncRoleProviders = roleProviders.getProviders(); final ActionListener descriptorsListener = ContextPreservingActionListener.wrapPreservingContext(ActionListener.wrap(ignore -> { rolesResult.setMissingRoles(roleNames); @@ -553,13 +545,12 @@ public void invalidate(Set roles) { } public void usageStats(ActionListener> listener) { - final Map usage = new HashMap<>(2); - usage.put("file", fileRolesStore.usageStats()); + final Map usage = new HashMap<>(); usage.put("dls", Map.of("bit_set_cache", dlsBitsetCache.usageStats())); - nativeRolesStore.usageStats(ActionListener.wrap(map -> { - usage.put("native", map); - listener.onResponse(usage); - }, listener::onFailure)); + roleProviders.usageStats(listener.map(roleUsage -> { + usage.putAll(roleUsage); + return usage; + })); } public void onSecurityIndexStateChange(SecurityIndexManager.State previousState, SecurityIndexManager.State currentState) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleProviders.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleProviders.java new file mode 100644 index 0000000000000..8e0252cb5c4fc --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleProviders.java @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz.store; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.security.Security; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; + +/** + * Encapsulates logic regarding the active set of role providers in the system, and their order + * The supported providers are (in order): + * - built in (reserved) roles + * - file-based roles + * - index-based roles + * - custom (plugin) providers. + * The set of permitted role providers can change due to changes in the license state. + */ +public class RoleProviders { + + private final List changeListeners; + + private final FileRolesStore fileRolesStore; + private final NativeRolesStore nativeRolesStore; + private final ReservedRolesStore reservedRolesStore; + + private final Map, ActionListener>>> customRoleProviders; + + private final XPackLicenseState licenseState; + + private List, ActionListener>> activeRoleProviders; + + public RoleProviders( + ReservedRolesStore reservedRolesStore, + FileRolesStore fileRolesStore, + NativeRolesStore nativeRolesStore, + Map, ActionListener>>> customRoleProviders, + XPackLicenseState licenseState + ) { + this.changeListeners = new CopyOnWriteArrayList<>(); + + this.reservedRolesStore = Objects.requireNonNull(reservedRolesStore); + this.fileRolesStore = Objects.requireNonNull(fileRolesStore); + this.fileRolesStore.addListener(this::onRoleModification); + this.nativeRolesStore = Objects.requireNonNull(nativeRolesStore); + this.customRoleProviders = Objects.requireNonNull(customRoleProviders); + + this.licenseState = licenseState; + this.licenseState.addListener(this::onLicenseChange); + + this.activeRoleProviders = calculateActiveRoleProviders(); + } + + private void onLicenseChange() { + var previousProviders = activeRoleProviders; + activeRoleProviders = calculateActiveRoleProviders(); + if (activeRoleProviders.equals(previousProviders) == false) { + changeListeners.forEach(ChangeListener::providersChanged); + } + } + + private List, ActionListener>> calculateActiveRoleProviders() { + final List, ActionListener>> builtInRoleProviders = List.of( + reservedRolesStore, + fileRolesStore, + nativeRolesStore + ); + if (customRoleProviders.isEmpty()) { + return builtInRoleProviders; + } + + final List, ActionListener>> providers = new ArrayList<>(); + providers.addAll(builtInRoleProviders); + + final XPackLicenseState fixedLicenseState = this.licenseState.copyCurrentLicenseState(); + this.customRoleProviders.forEach((name, customProviders) -> { + if (Security.CUSTOM_ROLE_PROVIDERS_FEATURE.checkAndStartTracking(fixedLicenseState, name)) { + providers.addAll(customProviders); + } else { + Security.CUSTOM_ROLE_PROVIDERS_FEATURE.stopTracking(fixedLicenseState, name); + } + }); + return List.copyOf(providers); + } + + private void onRoleModification(Set roles) { + changeListeners.forEach(l -> l.rolesChanged(roles)); + } + + public void addChangeListener(ChangeListener listener) { + changeListeners.add(Objects.requireNonNull(listener)); + } + + public List, ActionListener>> getProviders() { + return this.activeRoleProviders; + } + + public void usageStats(ActionListener> listener) { + final Map fileUsage = fileRolesStore.usageStats(); + nativeRolesStore.usageStats( + listener.map(nativeUsage -> Map.ofEntries(Map.entry("file", fileUsage), Map.entry("native", nativeUsage))) + ); + } + + interface ChangeListener { + void rolesChanged(Set roles); + void providersChanged(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index de1cafd620023..5dabc78f5bd00 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -32,14 +32,13 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.license.LicenseStateListener; import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.license.License.OperationMode; -import org.elasticsearch.license.TestUtils.UpdatableLicenseState; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; @@ -77,6 +76,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchAction; +import org.elasticsearch.xpack.security.Security; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.audit.index.IndexNameResolver; import org.elasticsearch.xpack.security.authc.ApiKeyService; @@ -377,12 +377,18 @@ public void testNegativeLookupsCacheDisabled() { .put("xpack.security.authz.store.roles.negative_lookup_cache.max_size", 0) .build(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, - reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(settings), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + settings, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + null, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds) + ); verify(fileRolesStore).addListener(anyConsumer()); // adds a listener in ctor final String roleName = randomAlphaOfLengthBetween(1, 10); @@ -417,13 +423,22 @@ public void testNegativeLookupsAreNotCachedWithFailures() { final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); + final XPackLicenseState licenseState = new XPackLicenseState(() -> 0); + final RoleProviders roleProviders = buildRolesProvider(fileRolesStore, nativeRolesStore, reservedRolesStore, null, licenseState); final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = new CompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + roleProviders, + mock(NativePrivilegeStore.class), + new ThreadContext(SECURITY_ENABLED_SETTINGS), + licenseState, + cache, + mock(ApiKeyService.class), + mock(ServiceAccountService.class), + documentSubsetBitsetCache, + resolver, + rds -> effectiveRoleDescriptors.set(rds) + ); verify(fileRolesStore).addListener(anyConsumer()); // adds a listener in ctor final String roleName = randomAlphaOfLengthBetween(1, 10); @@ -506,13 +521,24 @@ public void testCustomRolesProviders() { })); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, inMemoryProvider2), - new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(() -> 0), - cache, mock(ApiKeyService.class), mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final Map, ActionListener>>> customRoleProviders = Map.of( + "custom", + List.of(inMemoryProvider1, inMemoryProvider2) + ); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + customRoleProviders, + null, + null, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds), + null + ); final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); @@ -739,13 +765,23 @@ public void testCustomRolesProviderFailures() throws Exception { (roles, listener) -> listener.onFailure(new Exception("fake failure")); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, failingProvider), - new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(() -> 0), - cache, mock(ApiKeyService.class), mock(ServiceAccountService.class), - documentSubsetBitsetCache, resolver, rds -> effectiveRoleDescriptors.set(rds)); + final Map, ActionListener>>> customRoleProviders = randomBoolean() + ? Map.of("custom", List.of(inMemoryProvider1, failingProvider)) + : Map.of("custom", List.of(inMemoryProvider1), "failing", List.of(failingProvider)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + customRoleProviders, + null, + null, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds), + null + ); final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); @@ -785,57 +821,66 @@ public void testCustomRolesProvidersLicensing() { return RoleRetrievalResult.success(descriptors); }); - UpdatableLicenseState xPackLicenseState = new UpdatableLicenseState(SECURITY_ENABLED_SETTINGS); - // these licenses don't allow custom role providers - xPackLicenseState.update(randomFrom(OperationMode.BASIC, OperationMode.GOLD, OperationMode.STANDARD), true, null); + final MockLicenseState xPackLicenseState = MockLicenseState.createMock(); + when(xPackLicenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(false); + final AtomicReference licenseListener = new AtomicReference<>(null); + MockLicenseState.acceptListeners(xPackLicenseState, licenseListener::set); + final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), - Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState, cache, - mock(ApiKeyService.class), mock(ServiceAccountService.class), - documentSubsetBitsetCache, resolver, rds -> effectiveRoleDescriptors.set(rds)); + final Map, ActionListener>>> customRoleProviders = Map.of( + "custom", + List.of(inMemoryProvider) + ); + CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + Settings.EMPTY, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + customRoleProviders, + null, + xPackLicenseState, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds), + null + ); Set roleNames = Sets.newHashSet("roleA"); PlainActionFuture future = new PlainActionFuture<>(); compositeRolesStore.roles(roleNames, future); Role role = future.actionGet(); - assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); + assertThat(effectiveRoleDescriptors.get(), hasSize(0)); effectiveRoleDescriptors.set(null); + verify(xPackLicenseState).disableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, "custom"); // no roles should've been populated, as the license doesn't permit custom role providers assertEquals(0, role.indices().groups().length); - compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), - Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState, cache, - mock(ApiKeyService.class), mock(ServiceAccountService.class), - documentSubsetBitsetCache, resolver, rds -> effectiveRoleDescriptors.set(rds)); - // these licenses allow custom role providers - xPackLicenseState.update(randomFrom(OperationMode.PLATINUM, OperationMode.ENTERPRISE, OperationMode.TRIAL), true, null); + when(xPackLicenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(true); + licenseListener.get().licenseStateChanged(); + roleNames = Sets.newHashSet("roleA"); future = new PlainActionFuture<>(); compositeRolesStore.roles(roleNames, future); role = future.actionGet(); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(roleA)); effectiveRoleDescriptors.set(null); + verify(xPackLicenseState).enableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, "custom"); // roleA should've been populated by the custom role provider, because the license allows it assertEquals(1, role.indices().groups().length); - // license expired, don't allow custom role providers - compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), - Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState, cache, - mock(ApiKeyService.class), mock(ServiceAccountService.class), - documentSubsetBitsetCache, resolver, rds -> effectiveRoleDescriptors.set(rds)); - xPackLicenseState.update(randomFrom(OperationMode.PLATINUM, OperationMode.ENTERPRISE, OperationMode.TRIAL), false, null); + when(xPackLicenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(false); + licenseListener.get().licenseStateChanged(); + roleNames = Sets.newHashSet("roleA"); future = new PlainActionFuture<>(); compositeRolesStore.roles(roleNames, future); role = future.actionGet(); assertEquals(0, role.indices().groups().length); - assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); + assertThat(effectiveRoleDescriptors.get(), hasSize(0)); + verify(xPackLicenseState, times(2)).disableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, "custom"); } private SecurityIndexManager.State dummyState(ClusterHealthStatus indexStatus) { @@ -857,18 +902,21 @@ public void testCacheClearOnIndexHealthChange() { doCallRealMethod().when(reservedRolesStore).accept(anySetOf(String.class), anyActionListener()); NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), anyActionListener()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> {}) { - @Override - public void invalidateAll() { - numInvalidation.incrementAndGet(); - } - }; + + CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + Settings.EMPTY, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + null, + null, + null, + null, + null, + null, + store -> numInvalidation.incrementAndGet() + ); int expectedInvalidation = 0; // existing to no longer present @@ -912,17 +960,20 @@ public void testCacheClearOnIndexOutOfDateChange() { doCallRealMethod().when(reservedRolesStore).accept(anySetOf(String.class), anyActionListener()); NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), anyActionListener()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, - fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, rds -> {}) { - @Override - public void invalidateAll() { - numInvalidation.incrementAndGet(); - } - }; + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + null, + null, + null, + null, + null, + null, + store -> numInvalidation.incrementAndGet() + ); compositeRolesStore.onSecurityIndexStateChange(dummyIndexState(false, null), dummyIndexState(true, null)); assertEquals(1, numInvalidation.get()); @@ -1012,14 +1063,20 @@ public void testDoesNotUseRolesStoreForXPacAndAsyncSearchUser() { }).when(nativeRolesStore).getRoleDescriptors(isASet(), anyActionListener()); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + null, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds) + ); verify(fileRolesStore).addListener(anyConsumer()); // adds a listener in ctor // test Xpack user short circuits to its own reserved role @@ -1055,14 +1112,19 @@ public void testGetRolesForSystemUserThrowsException() { }).when(nativeRolesStore).getRoleDescriptors(isASet(), anyActionListener()); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, mock(ApiKeyService.class), - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + null, + null, + null, + null, + rds -> effectiveRoleDescriptors.set(rds) + ); verify(fileRolesStore).addListener(anyConsumer()); // adds a listener in ctor IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> compositeRolesStore.getRoles(SystemUser.INSTANCE, null, null)); @@ -1096,14 +1158,19 @@ public void testApiKeyAuthUsesApiKeyService() throws Exception { return Void.TYPE; }).when(nativePrivStore).getPrivileges(anyCollectionOf(String.class), anyCollectionOf(String.class), anyActionListener()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, apiKeyService, - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + nativePrivStore, + null, + apiKeyService, + null, + null, + rds -> effectiveRoleDescriptors.set(rds) + ); AuditUtil.getOrGenerateRequestId(threadContext); final Version version = randomFrom(Version.CURRENT, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1)); final Authentication authentication = createApiKeyAuthentication(apiKeyService, createAuthentication(), @@ -1152,14 +1219,19 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws Exception { return Void.TYPE; }).when(nativePrivStore).getPrivileges(anyCollectionOf(String.class), anyCollectionOf(String.class), anyActionListener()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), cache, apiKeyService, - mock(ServiceAccountService.class), documentSubsetBitsetCache, resolver, - rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + nativePrivStore, + null, + apiKeyService, + null, + null, + rds -> effectiveRoleDescriptors.set(rds) + ); AuditUtil.getOrGenerateRequestId(threadContext); final Version version = randomFrom(Version.CURRENT, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1)); final Authentication authentication = createApiKeyAuthentication(apiKeyService, createAuthentication(), @@ -1286,21 +1358,16 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { return Void.TYPE; }).when(nativePrivStore).getPrivileges(anyCollectionOf(String.class), anyCollectionOf(String.class), anyActionListener()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, nativePrivStore, - Collections.emptyList(), - new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(() -> 0), - cache, + null, apiKeyService, - mock(ServiceAccountService.class), - documentSubsetBitsetCache, - resolver, + null, + null, rds -> effectiveRoleDescriptors.set(rds)); AuditUtil.getOrGenerateRequestId(threadContext); final BytesArray roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); @@ -1490,35 +1557,59 @@ private Role getAsyncSearchUserRole() { return compositeRolesStore.getAsyncSearchUserRole(); } - private CompositeRolesStore buildCompositeRolesStore(Settings settings, - @Nullable FileRolesStore fileRolesStore, - @Nullable NativeRolesStore nativeRolesStore, - @Nullable ReservedRolesStore reservedRolesStore, - @Nullable NativePrivilegeStore privilegeStore, - @Nullable XPackLicenseState licenseState, - @Nullable ApiKeyService apiKeyService, - @Nullable ServiceAccountService serviceAccountService, - @Nullable DocumentSubsetBitsetCache documentSubsetBitsetCache, - @Nullable Consumer> roleConsumer) { - if (fileRolesStore == null) { - fileRolesStore = mock(FileRolesStore.class); - doCallRealMethod().when(fileRolesStore).accept(anySetOf(String.class), anyActionListener()); - when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); - } - if (nativeRolesStore == null) { - nativeRolesStore = mock(NativeRolesStore.class); - doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), anyActionListener()); - doAnswer((invocationOnMock) -> { - @SuppressWarnings("unchecked") - ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; - callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); - return null; - }).when(nativeRolesStore).getRoleDescriptors(isASet(), anyActionListener()); - } - if (reservedRolesStore == null) { - reservedRolesStore = mock(ReservedRolesStore.class); - doCallRealMethod().when(reservedRolesStore).accept(anySetOf(String.class), anyActionListener()); + private CompositeRolesStore buildCompositeRolesStore( + Settings settings, + @Nullable FileRolesStore fileRolesStore, + @Nullable NativeRolesStore nativeRolesStore, + @Nullable ReservedRolesStore reservedRolesStore, + @Nullable NativePrivilegeStore privilegeStore, + @Nullable XPackLicenseState licenseState, + @Nullable ApiKeyService apiKeyService, + @Nullable ServiceAccountService serviceAccountService, + @Nullable DocumentSubsetBitsetCache documentSubsetBitsetCache, + @Nullable Consumer> roleConsumer + ) { + return buildCompositeRolesStore( + settings, + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + null, + privilegeStore, + licenseState, + apiKeyService, + serviceAccountService, + documentSubsetBitsetCache, + roleConsumer, + null + ); + } + + private CompositeRolesStore buildCompositeRolesStore( + Settings settings, + @Nullable FileRolesStore fileRolesStore, + @Nullable NativeRolesStore nativeRolesStore, + @Nullable ReservedRolesStore reservedRolesStore, + @Nullable Map, ActionListener>>> customRoleProviders, + @Nullable NativePrivilegeStore privilegeStore, + @Nullable XPackLicenseState licenseState, + @Nullable ApiKeyService apiKeyService, + @Nullable ServiceAccountService serviceAccountService, + @Nullable DocumentSubsetBitsetCache documentSubsetBitsetCache, + @Nullable Consumer> roleConsumer, + @Nullable Consumer onInvalidation) { + if (licenseState == null) { + licenseState = new XPackLicenseState(() -> 0); } + + final RoleProviders roleProviders = buildRolesProvider( + fileRolesStore, + nativeRolesStore, + reservedRolesStore, + customRoleProviders, + licenseState + ); + if (privilegeStore == null) { privilegeStore = mock(NativePrivilegeStore.class); doAnswer((invocationOnMock) -> { @@ -1529,9 +1620,6 @@ private CompositeRolesStore buildCompositeRolesStore(Settings settings, return null; }).when(privilegeStore).getPrivileges(isASet(), isASet(), anyActionListener()); } - if (licenseState == null) { - licenseState = new XPackLicenseState(() -> 0); - } if (apiKeyService == null) { apiKeyService = mock(ApiKeyService.class); } @@ -1544,14 +1632,76 @@ private CompositeRolesStore buildCompositeRolesStore(Settings settings, if (roleConsumer == null) { roleConsumer = rds -> { }; } - return new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, privilegeStore, - Collections.emptyList(), new ThreadContext(settings), licenseState, cache, apiKeyService, - serviceAccountService, documentSubsetBitsetCache, resolver, roleConsumer); + + return new CompositeRolesStore( + settings, + roleProviders, + privilegeStore, + new ThreadContext(settings), + licenseState, + cache, + apiKeyService, + serviceAccountService, + documentSubsetBitsetCache, + resolver, + roleConsumer + ) { + @Override + public void invalidateAll() { + if (onInvalidation == null) { + super.invalidateAll(); + } else { + onInvalidation.accept(this); + } + } + }; + } + + private RoleProviders buildRolesProvider( + @Nullable FileRolesStore fileRolesStore, + @Nullable NativeRolesStore nativeRolesStore, + @Nullable ReservedRolesStore reservedRolesStore, + @Nullable Map, ActionListener>>> customRoleProviders, + @Nullable XPackLicenseState licenseState + ) { + if (fileRolesStore == null) { + fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(anySetOf(String.class), anyActionListener()); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + } + if (nativeRolesStore == null) { + nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), anyActionListener()); + doAnswer((invocationOnMock) -> { + @SuppressWarnings("unchecked") + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isASet(), anyActionListener()); + } + if (reservedRolesStore == null) { + reservedRolesStore = mock(ReservedRolesStore.class); + doCallRealMethod().when(reservedRolesStore).accept(anySetOf(String.class), anyActionListener()); + } + if (licenseState == null) { + licenseState = new XPackLicenseState(() -> 0); + } + if (customRoleProviders == null) { + customRoleProviders = Map.of(); + } + return new RoleProviders( + reservedRolesStore, + fileRolesStore, + nativeRolesStore, + customRoleProviders, + licenseState + ); } private DocumentSubsetBitsetCache buildBitsetCache() { return new DocumentSubsetBitsetCache(Settings.EMPTY, mock(ThreadPool.class)); } + private static class InMemoryRolesProvider implements BiConsumer, ActionListener> { private final Function, RoleRetrievalResult> roleDescriptorsFunc; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/RoleProvidersTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/RoleProvidersTests.java new file mode 100644 index 0000000000000..41081230f720a --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/RoleProvidersTests.java @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz.store; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.license.LicenseStateListener; +import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.security.Security; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RoleProvidersTests extends ESTestCase { + + public void testFileRoleInvalidationListener() { + final MockLicenseState licenseState = MockLicenseState.createMock(); + + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + AtomicReference>> fileChangeListener = new AtomicReference<>(null); + doAnswer(inv -> { + @SuppressWarnings("unchecked") + final Consumer> consumer = (Consumer>) inv.getArguments()[0]; + fileChangeListener.set(consumer); + return null; + }).when(fileRolesStore).addListener(any()); + + final RoleProviders roleProviders = new RoleProviders( + mock(ReservedRolesStore.class), + fileRolesStore, + mock(NativeRolesStore.class), + Map.of(), + licenseState + ); + assertThat(fileChangeListener.get(), notNullValue()); + + final AtomicInteger roleChangeCount = new AtomicInteger(0); + final AtomicReference> lastRolesChange = new AtomicReference<>(Set.of()); + + roleProviders.addChangeListener(new RoleProviders.ChangeListener() { + @Override + public void rolesChanged(Set roles) { + roleChangeCount.incrementAndGet(); + lastRolesChange.set(roles); + } + + @Override + public void providersChanged() { + // ignore + } + }); + + assertThat(roleProviders.getProviders(), hasSize(3)); + assertThat(roleChangeCount.get(), is(0)); + + assertThat(roleProviders.getProviders(), hasSize(3)); + assertThat(roleChangeCount.get(), is(0)); + + Set roleNames = Sets.newHashSet(generateRandomStringArray(5, 8, false, false)); + fileChangeListener.get().accept(roleNames); + assertThat(roleProviders.getProviders(), hasSize(3)); + assertThat(roleChangeCount.get(), is(1)); + assertThat(lastRolesChange.get(), is(roleNames)); + + roleNames = Sets.newHashSet(generateRandomStringArray(5, 4, false, false)); + fileChangeListener.get().accept(roleNames); + assertThat(roleProviders.getProviders(), hasSize(3)); + assertThat(roleChangeCount.get(), is(2)); + assertThat(lastRolesChange.get(), is(roleNames)); + } + + public void testLicenseChange() { + final MockLicenseState licenseState = MockLicenseState.createMock(); + when(licenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(true); + + final AtomicReference licenseListener = new AtomicReference<>(null); + MockLicenseState.acceptListeners(licenseState, licenseListener::set); + + List, ActionListener>> customProviders = List.of((names, listener) -> {}); + final String extensionName = randomAlphaOfLengthBetween(3, 12); + final RoleProviders roleProviders = new RoleProviders( + mock(ReservedRolesStore.class), + mock(FileRolesStore.class), + mock(NativeRolesStore.class), + Map.of(extensionName, customProviders), + licenseState + ); + assertThat(licenseListener.get(), notNullValue()); + + final AtomicInteger providerChangeCount = new AtomicInteger(0); + roleProviders.addChangeListener(new RoleProviders.ChangeListener() { + @Override + public void rolesChanged(Set roles) { + // ignore + } + + @Override + public void providersChanged() { + providerChangeCount.incrementAndGet(); + } + }); + + assertThat(roleProviders.getProviders(), hasSize(4)); + assertThat(providerChangeCount.get(), is(0)); + verify(licenseState, times(1)).enableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + + licenseListener.get().licenseStateChanged(); + assertThat(roleProviders.getProviders(), hasSize(4)); + assertThat(providerChangeCount.get(), is(0)); // no relevant change in license + verify(licenseState, times(2)).enableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + + when(licenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(false); + licenseListener.get().licenseStateChanged(); + assertThat(roleProviders.getProviders(), hasSize(3)); + assertThat(providerChangeCount.get(), is(1)); + verify(licenseState, times(2)).enableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + verify(licenseState, times(1)).disableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + + when(licenseState.isAllowed(Security.CUSTOM_ROLE_PROVIDERS_FEATURE)).thenReturn(true); + licenseListener.get().licenseStateChanged(); + assertThat(roleProviders.getProviders(), hasSize(4)); + assertThat(providerChangeCount.get(), is(2)); + verify(licenseState, times(3)).enableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + verify(licenseState, times(1)).disableUsageTracking(Security.CUSTOM_ROLE_PROVIDERS_FEATURE, extensionName); + } + +}