diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackUsageAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackUsageAction.java index 6b7d5b96d2024..554e8e076660f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackUsageAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackUsageAction.java @@ -53,8 +53,7 @@ protected XPackUsageResponse newResponse() { } @Override - protected void masterOperation(XPackUsageRequest request, ClusterState state, ActionListener listener) - throws Exception { + protected void masterOperation(XPackUsageRequest request, ClusterState state, ActionListener listener) { final ActionListener> usageActionListener = new ActionListener>() { @Override public void onResponse(List usages) { @@ -73,7 +72,8 @@ public void onFailure(Exception e) { @Override public void onResponse(Usage usage) { featureSetUsages.set(position.getAndIncrement(), usage); - iteratingListener.onResponse(null); // just send null back and keep iterating + // the value sent back doesn't matter since our predicate keeps iterating + iteratingListener.onResponse(Collections.emptyList()); } @Override @@ -84,13 +84,13 @@ public void onFailure(Exception e) { }; IteratingActionListener, XPackFeatureSet> iteratingActionListener = new IteratingActionListener<>(usageActionListener, consumer, featureSets, - threadPool.getThreadContext(), () -> { + threadPool.getThreadContext(), (ignore) -> { final List usageList = new ArrayList<>(featureSetUsages.length()); for (int i = 0; i < featureSetUsages.length(); i++) { usageList.add(featureSetUsages.get(i)); } return usageList; - }); + }, (ignore) -> true); iteratingActionListener.run(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/IteratingActionListener.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/IteratingActionListener.java index 46ebd89b8ea76..7eb3a01b44129 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/IteratingActionListener.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/common/IteratingActionListener.java @@ -6,12 +6,14 @@ package org.elasticsearch.xpack.core.common; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.Nullable; import org.elasticsearch.common.util.concurrent.ThreadContext; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -32,7 +34,8 @@ public final class IteratingActionListener implements ActionListener, R private final ActionListener delegate; private final BiConsumer> consumer; private final ThreadContext threadContext; - private final Supplier consumablesFinishedResponse; + private final Function finalResultFunction; + private final Predicate iterationPredicate; private int position = 0; @@ -46,7 +49,7 @@ public final class IteratingActionListener implements ActionListener, R */ public IteratingActionListener(ActionListener delegate, BiConsumer> consumer, List consumables, ThreadContext threadContext) { - this(delegate, consumer, consumables, threadContext, null); + this(delegate, consumer, consumables, threadContext, Function.identity()); } /** @@ -56,18 +59,36 @@ public IteratingActionListener(ActionListener delegate, BiConsumer delegate, BiConsumer> consumer, List consumables, - ThreadContext threadContext, @Nullable Supplier consumablesFinishedResponse) { + ThreadContext threadContext, Function finalResultFunction) { + this(delegate, consumer, consumables, threadContext, finalResultFunction, Objects::isNull); + } + + /** + * Constructs an {@link IteratingActionListener}. + * + * @param delegate the delegate listener to call when all consumables have finished executing + * @param consumer the consumer that is executed for each consumable instance + * @param consumables the instances that can be consumed to produce a response which is ultimately sent on the delegate listener + * @param threadContext the thread context for the thread pool that created the listener + * @param finalResultFunction a function that maps the response which terminated iteration to a response that will be sent to the + * delegate listener. This is useful if the delegate listener should receive some other value (perhaps + * a concatenation of the results of all the called consumables). + * @param iterationPredicate a {@link Predicate} that checks if iteration should continue based on the returned result + */ + public IteratingActionListener(ActionListener delegate, BiConsumer> consumer, List consumables, + ThreadContext threadContext, Function finalResultFunction, + Predicate iterationPredicate) { this.delegate = delegate; this.consumer = consumer; this.consumables = Collections.unmodifiableList(consumables); this.threadContext = threadContext; - this.consumablesFinishedResponse = consumablesFinishedResponse; + this.finalResultFunction = finalResultFunction; + this.iterationPredicate = iterationPredicate; } @Override @@ -88,18 +109,15 @@ public void onResponse(T response) { // we need to store the context here as there is a chance that this method is called from a thread outside of the ThreadPool // like a LDAP connection reader thread and we can pollute the context in certain cases try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(false)) { - if (response == null) { + final boolean continueIteration = iterationPredicate.test(response); + if (continueIteration) { if (position == consumables.size()) { - if (consumablesFinishedResponse != null) { - delegate.onResponse(consumablesFinishedResponse.get()); - } else { - delegate.onResponse(null); - } + delegate.onResponse(finalResultFunction.apply(response)); } else { consumer.accept(consumables.get(position++), this); } } else { - delegate.onResponse(response); + delegate.onResponse(finalResultFunction.apply(response)); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index 190e9f7520b6c..f422d073cfeb5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import java.util.ArrayList; import java.util.Collections; @@ -72,16 +73,20 @@ default AuthenticationFailureHandler getAuthenticationFailureHandler() { * should be asynchronous if the computation is lengthy or any disk and/or network * I/O is involved. The implementation is responsible for resolving whatever roles * it can into a set of {@link RoleDescriptor} instances. If successful, the - * implementation must invoke {@link ActionListener#onResponse(Object)} to pass along - * the resolved set of role descriptors. If a failure was encountered, the - * implementation must invoke {@link ActionListener#onFailure(Exception)}. + * implementation must wrap the set of {@link RoleDescriptor} instances in a + * {@link RoleRetrievalResult} using {@link RoleRetrievalResult#success(Set)} and then invoke + * {@link ActionListener#onResponse(Object)}. If a failure was encountered, the + * implementation should wrap the failure in a {@link RoleRetrievalResult} using + * {@link RoleRetrievalResult#failure(Exception)} and then invoke + * {@link ActionListener#onResponse(Object)} unless the failure needs to terminate the request, + * in which case the implementation should invoke {@link ActionListener#onFailure(Exception)}. * * By default, an empty list is returned. * * @param settings The configured settings for the node * @param resourceWatcherService Use to watch configuration files for changes */ - default List, ActionListener>>> + default List, ActionListener>> getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) { return Collections.emptyList(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleRetrievalResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleRetrievalResult.java new file mode 100644 index 0000000000000..ea18c94021674 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleRetrievalResult.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.Objects; +import java.util.Set; + +/** + * The result of attempting to retrieve roles from a roles provider. The result can either be + * successful or a failure. A successful result indicates that no errors occurred while retrieving + * roles, even if none of the requested roles could be found. A failure indicates an error + * occurred while retrieving the results but the error is not fatal and the request may be able + * to continue. + */ +public final class RoleRetrievalResult { + + private final Set descriptors; + private final Exception failure; + + private RoleRetrievalResult(Set descriptors, Exception failure) { + if (descriptors != null && failure != null) { + throw new IllegalArgumentException("either descriptors or failure must be null"); + } + this.descriptors = descriptors; + this.failure = failure; + } + + /** + * @return the resolved descriptors or {@code null} if there was a failure + */ + public Set getDescriptors() { + return descriptors; + } + + /** + * @return the failure or {@code null} if retrieval succeeded + */ + public Exception getFailure() { + return failure; + } + + /** + * @return true if the retrieval succeeded + */ + public boolean isSuccess() { + return descriptors != null; + } + + /** + * Creates a successful result with the provided {@link RoleDescriptor} set, + * which must be non-null + */ + public static RoleRetrievalResult success(Set descriptors) { + Objects.requireNonNull(descriptors, "descriptors must not be null if successful"); + return new RoleRetrievalResult(descriptors, null); + } + + /** + * Creates a failed result with the provided non-null exception + */ + public static RoleRetrievalResult failure(Exception e) { + Objects.requireNonNull(e, "Exception must be provided"); + return new RoleRetrievalResult(null, e); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/IteratingActionListenerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/IteratingActionListenerTests.java index 0648d774075d5..9b134a75b3f7c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/IteratingActionListenerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/common/IteratingActionListenerTests.java @@ -7,7 +7,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.collect.HppcMaps.Object; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; @@ -18,8 +17,12 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; public class IteratingActionListenerTests extends ESTestCase { @@ -136,4 +139,49 @@ public void testFailure() { assertEquals(numberOfIterations, iterations.get()); assertTrue(onFailureCalled.get()); } + + public void testFunctionApplied() { + final int numberOfItems = scaledRandomIntBetween(2, 32); + final int numberOfIterations = scaledRandomIntBetween(1, numberOfItems); + List items = new ArrayList<>(numberOfItems); + for (int i = 0; i < numberOfItems; i++) { + items.add(new Object()); + } + + final AtomicInteger iterations = new AtomicInteger(0); + final Predicate iterationPredicate = object -> { + final int current = iterations.incrementAndGet(); + return current != numberOfIterations; + }; + final BiConsumer> consumer = (listValue, listener) -> { + listener.onResponse(items.get(iterations.get())); + }; + + final AtomicReference originalObject = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); + final Function responseFunction = object -> { + originalObject.set(object); + Object randomResult; + do { + randomResult = randomFrom(items); + } while (randomResult == object); + result.set(randomResult); + return randomResult; + }; + + IteratingActionListener iteratingListener = new IteratingActionListener<>(ActionListener.wrap((object) -> { + assertNotNull(object); + assertNotNull(originalObject.get()); + assertThat(object, sameInstance(result.get())); + assertThat(object, not(sameInstance(originalObject.get()))); + assertThat(originalObject.get(), sameInstance(items.get(iterations.get() - 1))); + }, (e) -> { + logger.error("unexpected exception", e); + fail("exception should not have been thrown"); + }), consumer, items, new ThreadContext(Settings.EMPTY), responseFunction, iterationPredicate); + iteratingListener.run(); + + // we never really went async, its all chained together so verify this for sanity + assertEquals(numberOfIterations, iterations.get()); + } } 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 76b1a87f682fa..2fcad75c40463 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 @@ -116,7 +116,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.accesscontrol.SecurityIndexSearcherWrapper; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -184,6 +183,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.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; @@ -458,7 +458,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, getLicenseState()); final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, getLicenseState(), securityIndex.get()); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); - List, ActionListener>>> rolesProviders = new ArrayList<>(); + List, ActionListener>> rolesProviders = new ArrayList<>(); for (SecurityExtension extension : securityExtensions) { rolesProviders.addAll(extension.getRolesProviders(settings, resourceWatcherService)); } @@ -610,7 +610,7 @@ public static List> getSettings(boolean transportClientMode, List { - roles.addAll(foundRoles); - listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); - }, listener::onFailure)); + nativeRolesStore.getRoleDescriptors(roleNames, ActionListener.wrap((retrievalResult) -> { + if (retrievalResult.isSuccess()) { + roles.addAll(retrievalResult.getDescriptors()); + listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); + } else { + listener.onFailure(retrievalResult.getFailure()); + } + }, listener::onFailure)); } } } 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 7e1cc49e2c0bc..634d0017be262 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 @@ -17,7 +17,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; @@ -34,6 +33,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; @@ -51,10 +51,11 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.elasticsearch.common.util.set.Sets.newHashSet; -import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isIndexDeleted; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isMoveFromRedToNonRed; @@ -77,8 +78,10 @@ public class CompositeRolesStore extends AbstractComponent { writeLock = new ReleasableLock(iterationLock.writeLock()); } - public static final Setting CACHE_SIZE_SETTING = - Setting.intSetting(setting("authz.store.roles.cache.max_size"), 10000, Property.NodeScope); + private static final Setting CACHE_SIZE_SETTING = + Setting.intSetting("xpack.security.authz.store.roles.cache.max_size", 10000, Property.NodeScope); + private static final Setting NEGATIVE_LOOKUP_CACHE_SIZE_SETTING = + Setting.intSetting("xpack.security.authz.store.roles.negative_lookup_cache.max_size", 10000, Property.NodeScope); private final FileRolesStore fileRolesStore; private final NativeRolesStore nativeRolesStore; @@ -86,14 +89,14 @@ public class CompositeRolesStore extends AbstractComponent { private final NativePrivilegeStore privilegeStore; private final XPackLicenseState licenseState; private final Cache, Role> roleCache; - private final Set negativeLookupCache; + private final Cache negativeLookupCache; private final ThreadContext threadContext; private final AtomicLong numInvalidation = new AtomicLong(); - private final List, ActionListener>>> customRolesProviders; + private final List, ActionListener>> customRolesProviders; public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore, ReservedRolesStore reservedRolesStore, NativePrivilegeStore privilegeStore, - List, ActionListener>>> rolesProviders, + List, ActionListener>> rolesProviders, ThreadContext threadContext, XPackLicenseState licenseState) { super(settings); this.fileRolesStore = fileRolesStore; @@ -109,7 +112,12 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat } this.roleCache = builder.build(); this.threadContext = threadContext; - this.negativeLookupCache = ConcurrentCollections.newConcurrentSet(); + CacheBuilder nlcBuilder = CacheBuilder.builder(); + final int nlcCacheSize = NEGATIVE_LOOKUP_CACHE_SIZE_SETTING.get(settings); + if (cacheSize >= 0) { + builder.setMaximumWeight(nlcCacheSize); + } + this.negativeLookupCache = nlcBuilder.build(); this.customRolesProviders = Collections.unmodifiableList(rolesProviders); } @@ -120,28 +128,39 @@ public void roles(Set roleNames, FieldPermissionsCache fieldPermissionsC } else { final long invalidationCounter = numInvalidation.get(); roleDescriptors(roleNames, ActionListener.wrap( - descriptors -> { + rolesRetrievalResult -> { + final boolean missingRoles = rolesRetrievalResult.getMissingRoles().isEmpty() == false; + if (missingRoles) { + logger.debug("Could not find roles with names {}", rolesRetrievalResult.getMissingRoles()); + } + final Set effectiveDescriptors; if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { - effectiveDescriptors = descriptors; + effectiveDescriptors = rolesRetrievalResult.getRoleDescriptors(); } else { - effectiveDescriptors = descriptors.stream() + effectiveDescriptors = rolesRetrievalResult.getRoleDescriptors().stream() .filter((rd) -> rd.isUsingDocumentOrFieldLevelSecurity() == false) .collect(Collectors.toSet()); } logger.trace("Building role from descriptors [{}] for names [{}]", effectiveDescriptors, roleNames); buildRoleFromDescriptors(effectiveDescriptors, fieldPermissionsCache, privilegeStore, ActionListener.wrap(role -> { if (role != null) { - try (ReleasableLock ignored = readLock.acquire()) { - /* this is kinda spooky. We use a read/write lock to ensure we don't modify the cache if we hold - * the write lock (fetching stats for instance - which is kinda overkill?) but since we fetching - * stuff in an async fashion we need to make sure that if the cache got invalidated since we - * started the request we don't put a potential stale result in the cache, hence the - * numInvalidation.get() comparison to the number of invalidation when we started. we just try to - * be on the safe side and don't cache potentially stale results - */ - if (invalidationCounter == numInvalidation.get()) { - roleCache.computeIfAbsent(roleNames, (s) -> role); + if (rolesRetrievalResult.hadFailures() == false) { + try (ReleasableLock ignored = readLock.acquire()) { + /* this is kinda spooky. We use a read/write lock to ensure we don't modify the cache if we hold + * the write lock (fetching stats for instance - which is kinda overkill?) but since we fetching + * stuff in an async fashion we need to make sure that if the cache got invalidated since we + * started the request we don't put a potential stale result in the cache, hence the + * numInvalidation.get() comparison to the number of invalidation when we started. we just try to + * be on the safe side and don't cache potentially stale results + */ + if (invalidationCounter == numInvalidation.get()) { + roleCache.computeIfAbsent(roleNames, (s) -> role); + } + } + + for (String missingRole : rolesRetrievalResult.getMissingRoles()) { + negativeLookupCache.computeIfAbsent(missingRole, s -> Boolean.TRUE); } } } @@ -152,88 +171,113 @@ public void roles(Set roleNames, FieldPermissionsCache fieldPermissionsC } } - private void roleDescriptors(Set roleNames, ActionListener> roleDescriptorActionListener) { + private void roleDescriptors(Set roleNames, ActionListener rolesResultListener) { final Set filteredRoleNames = roleNames.stream().filter((s) -> { - if (negativeLookupCache.contains(s)) { + if (negativeLookupCache.get(s) != null) { logger.debug("Requested role [{}] does not exist (cached)", s); return false; } else { return true; } }).collect(Collectors.toSet()); + final RolesRetrievalResult retrievalResult = new RolesRetrievalResult(); final Set builtInRoleDescriptors = getBuiltInRoleDescriptors(filteredRoleNames); + retrievalResult.addDescriptors(builtInRoleDescriptors); Set remainingRoleNames = difference(filteredRoleNames, builtInRoleDescriptors); + if (remainingRoleNames.isEmpty()) { - roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors)); + rolesResultListener.onResponse(retrievalResult); } else { - nativeRolesStore.getRoleDescriptors(remainingRoleNames.toArray(Strings.EMPTY_ARRAY), ActionListener.wrap((descriptors) -> { - logger.debug(() -> new ParameterizedMessage("Roles [{}] were resolved from the native index store", names(descriptors))); - builtInRoleDescriptors.addAll(descriptors); - callCustomRoleProvidersIfEnabled(builtInRoleDescriptors, filteredRoleNames, roleDescriptorActionListener); - }, e -> { - logger.warn("role retrieval failed from the native roles store", e); - callCustomRoleProvidersIfEnabled(builtInRoleDescriptors, filteredRoleNames, roleDescriptorActionListener); - })); + final Set fileRoleDescriptors = getDescriptorsFromFileStore(remainingRoleNames); + retrievalResult.addDescriptors(fileRoleDescriptors); + remainingRoleNames = difference(remainingRoleNames, fileRoleDescriptors); + if (remainingRoleNames.isEmpty()) { + rolesResultListener.onResponse(retrievalResult); + } else { + loadNativeRoleDescriptors(retrievalResult, rolesResultListener, remainingRoleNames); + } } } - private void callCustomRoleProvidersIfEnabled(Set builtInRoleDescriptors, Set filteredRoleNames, - ActionListener> roleDescriptorActionListener) { - if (builtInRoleDescriptors.size() != filteredRoleNames.size()) { - final Set missing = difference(filteredRoleNames, builtInRoleDescriptors); - assert missing.isEmpty() == false : "the missing set should not be empty if the sizes didn't match"; - if (licenseState.isCustomRoleProvidersAllowed() && !customRolesProviders.isEmpty()) { - new IteratingActionListener<>(roleDescriptorActionListener, (rolesProvider, listener) -> { - // resolve descriptors with role provider - rolesProvider.accept(missing, ActionListener.wrap((resolvedDescriptors) -> { - logger.debug(() -> - new ParameterizedMessage("Roles [{}] were resolved by [{}]", names(resolvedDescriptors), rolesProvider)); - builtInRoleDescriptors.addAll(resolvedDescriptors); - // remove resolved descriptors from the set of roles still needed to be resolved - for (RoleDescriptor descriptor : resolvedDescriptors) { - missing.remove(descriptor.getName()); - } - if (missing.isEmpty()) { - // no more roles to resolve, send the response - listener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors)); - } else { - // still have roles to resolve, keep trying with the next roles provider - listener.onResponse(null); - } - }, listener::onFailure)); - }, customRolesProviders, threadContext, () -> { - negativeLookupCache.addAll(missing); - return builtInRoleDescriptors; - }).run(); + private void loadNativeRoleDescriptors(RolesRetrievalResult rolesResult, ActionListener listener, + Set roleNames) { + nativeRolesStore.getRoleDescriptors(roleNames.toArray(Strings.EMPTY_ARRAY), ActionListener.wrap((result) -> { + if (result.isSuccess()) { + final Set descriptors = result.getDescriptors(); + logger.debug( + () -> new ParameterizedMessage("Roles [{}] were resolved from the native index store", names(descriptors))); + rolesResult.addDescriptors(descriptors); + final Set remainingRoles = difference(roleNames, descriptors); + if (remainingRoles.isEmpty()) { + listener.onResponse(rolesResult); + } else { + callCustomRoleProvidersIfEnabled(rolesResult, remainingRoles, listener); + } } else { - logger.debug(() -> - new ParameterizedMessage("Requested roles [{}] do not exist", Strings.collectionToCommaDelimitedString(missing))); - negativeLookupCache.addAll(missing); - roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors)); + logger.warn("role retrieval failed from the native roles store", result.getFailure()); + rolesResult.setUnsuccessful(); + callCustomRoleProvidersIfEnabled(rolesResult, roleNames, listener); } + }, listener::onFailure)); + } + + private void callCustomRoleProvidersIfEnabled(RolesRetrievalResult rolesResult, Set remainingRoleNames, + ActionListener resultListener) { + if (licenseState.isCustomRoleProvidersAllowed() && customRolesProviders.isEmpty() == false) { + final ActionListener descriptorsListener = ActionListener.wrap(ignore -> { + rolesResult.setMissingRoles(remainingRoleNames); + resultListener.onResponse(rolesResult); + }, resultListener::onFailure); + + final Predicate iterationPredicate = result -> { + if (result.isSuccess()) { + final Set resolvedDescriptors = result.getDescriptors(); + rolesResult.addDescriptors(resolvedDescriptors); + // remove resolved descriptors from the set of roles still needed to be resolved + for (RoleDescriptor descriptor : resolvedDescriptors) { + remainingRoleNames.remove(descriptor.getName()); + } + } else { + rolesResult.setUnsuccessful(); + } + return remainingRoleNames.isEmpty() == false; + }; + + new IteratingActionListener<>(descriptorsListener, (rolesProvider, listener) -> { + // try to resolve descriptors with role provider + rolesProvider.accept(remainingRoleNames, ActionListener.wrap(result -> { + if (result.isSuccess()) { + logger.debug(() -> new ParameterizedMessage("Roles [{}] were resolved by [{}]", + names(result.getDescriptors()), rolesProvider)); + } else { + logger.warn(new ParameterizedMessage("role retrieval failed from [{}]", rolesProvider), result.getFailure()); + } + listener.onResponse(result); + }, listener::onFailure)); + }, customRolesProviders, threadContext, Function.identity(), iterationPredicate).run(); } else { - roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors)); + rolesResult.setMissingRoles(remainingRoleNames); + resultListener.onResponse(rolesResult); } } private Set getBuiltInRoleDescriptors(Set roleNames) { final Set descriptors = reservedRolesStore.roleDescriptors().stream() .filter((rd) -> roleNames.contains(rd.getName())) - .collect(Collectors.toCollection(HashSet::new)); + .collect(Collectors.toSet()); if (descriptors.size() > 0) { logger.debug(() -> new ParameterizedMessage("Roles [{}] are builtin roles", names(descriptors))); } - final Set difference = difference(roleNames, descriptors); - if (difference.isEmpty() == false) { - final Set fileRoles = fileRolesStore.roleDescriptors(difference); - logger.debug(() -> - new ParameterizedMessage("Roles [{}] were resolved from [{}]", names(fileRoles), fileRolesStore.getFile())); - descriptors.addAll(fileRoles); - } - return descriptors; } + private Set getDescriptorsFromFileStore(Set roleNames) { + final Set fileRoles = fileRolesStore.roleDescriptors(roleNames); + logger.debug(() -> + new ParameterizedMessage("Roles [{}] were resolved from [{}]", names(fileRoles), fileRolesStore.getFile())); + return fileRoles; + } + private String names(Collection descriptors) { return descriptors.stream().map(RoleDescriptor::getName).collect(Collectors.joining(",")); } @@ -332,7 +376,7 @@ public static void buildRoleFromDescriptors(Collection roleDescr public void invalidateAll() { numInvalidation.incrementAndGet(); - negativeLookupCache.clear(); + negativeLookupCache.invalidateAll(); try (ReleasableLock ignored = readLock.acquire()) { roleCache.invalidateAll(); } @@ -351,7 +395,7 @@ public void invalidate(String role) { } } } - negativeLookupCache.remove(role); + negativeLookupCache.invalidate(role); } public void invalidate(Set roles) { @@ -368,7 +412,7 @@ public void invalidate(Set roles) { } } - negativeLookupCache.removeAll(roles); + roles.forEach(negativeLookupCache::invalidate); } public void usageStats(ActionListener> listener) { @@ -421,4 +465,39 @@ void merge(MergeableIndicesPrivilege other) { } } } + + private static final class RolesRetrievalResult { + + private final Set roleDescriptors = new HashSet<>(); + private Set missingRoles = Collections.emptySet(); + private boolean allSuccessful = true; + + private void addDescriptors(Set descriptors) { + roleDescriptors.addAll(descriptors); + } + + private Set getRoleDescriptors() { + return roleDescriptors; + } + + private void setUnsuccessful() { + allSuccessful = false; + } + + private boolean hadFailures() { + return allSuccessful == false; + } + + private void setMissingRoles(Set missingRoles) { + this.missingRoles = missingRoles; + } + + private Set getMissingRoles() { + return missingRoles; + } + } + + public static List> getSettings() { + return Arrays.asList(CACHE_SIZE_SETTING, NEGATIVE_LOOKUP_CACHE_SIZE_SETTING); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java index e032d524038a7..6771eb841085a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java @@ -19,7 +19,6 @@ import org.elasticsearch.action.search.MultiSearchResponse.Item; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.ContextPreservingActionListener; -import org.elasticsearch.action.support.TransportActions; import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; @@ -43,6 +42,7 @@ import org.elasticsearch.xpack.core.security.action.role.PutRoleRequest; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -52,6 +52,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -101,14 +102,12 @@ public NativeRolesStore(Settings settings, Client client, XPackLicenseState lice /** * Retrieve a list of roles, if rolesToGet is null or empty, fetch all roles */ - public void getRoleDescriptors(String[] names, final ActionListener> listener) { + public void getRoleDescriptors(String[] names, final ActionListener listener) { if (securityIndex.indexExists() == false) { // TODO remove this short circuiting and fix tests that fail without this! - listener.onResponse(Collections.emptyList()); + listener.onResponse(RoleRetrievalResult.success(Collections.emptySet())); } else if (names != null && names.length == 1) { - getRoleDescriptor(Objects.requireNonNull(names[0]), ActionListener.wrap(roleDescriptor -> - listener.onResponse(roleDescriptor == null ? Collections.emptyList() : Collections.singletonList(roleDescriptor)), - listener::onFailure)); + getRoleDescriptor(Objects.requireNonNull(names[0]), listener); } else { securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { QueryBuilder query; @@ -127,7 +126,10 @@ public void getRoleDescriptors(String[] names, final ActionListener(supplier, listener), + final ActionListener> descriptorsListener = ActionListener.wrap( + roleDescriptors -> listener.onResponse(RoleRetrievalResult.success(new HashSet<>(roleDescriptors))), + e -> listener.onResponse(RoleRetrievalResult.failure(e))); + ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, descriptorsListener), (hit) -> transformRole(hit.getId(), hit.getSourceRef(), logger, licenseState)); } }); @@ -261,30 +263,23 @@ public void onFailure(Exception e) { } } - private void getRoleDescriptor(final String roleId, ActionListener roleActionListener) { + private void getRoleDescriptor(final String roleId, ActionListener resultListener) { if (securityIndex.indexExists() == false) { // TODO remove this short circuiting and fix tests that fail without this! - roleActionListener.onResponse(null); + resultListener.onResponse(RoleRetrievalResult.success(Collections.emptySet())); } else { - securityIndex.prepareIndexIfNeededThenExecute(roleActionListener::onFailure, () -> + securityIndex.prepareIndexIfNeededThenExecute(e -> resultListener.onResponse(RoleRetrievalResult.failure(e)), () -> executeGetRoleRequest(roleId, new ActionListener() { @Override public void onResponse(GetResponse response) { final RoleDescriptor descriptor = transformRole(response); - roleActionListener.onResponse(descriptor); + resultListener.onResponse(RoleRetrievalResult.success( + descriptor == null ? Collections.emptySet() : Collections.singleton(descriptor))); } @Override public void onFailure(Exception e) { - // if the index or the shard is not there / available we just claim the role is not there - if (TransportActions.isShardNotAvailableException(e)) { - logger.warn((org.apache.logging.log4j.util.Supplier) () -> - new ParameterizedMessage("failed to load role [{}] index not available", roleId), e); - roleActionListener.onResponse(null); - } else { - logger.error(new ParameterizedMessage("failed to load role [{}]", roleId), e); - roleActionListener.onFailure(e); - } + resultListener.onResponse(RoleRetrievalResult.failure(e)); } })); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java index d269de25c612d..6ec1e3b3d2084 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRolesCacheTests.java @@ -11,7 +11,7 @@ import org.elasticsearch.xpack.core.security.action.role.DeleteRoleResponse; import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; import org.elasticsearch.xpack.core.security.action.role.PutRoleResponse; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -19,7 +19,6 @@ import org.junit.BeforeClass; import java.util.Arrays; -import java.util.Collection; import java.util.List; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; @@ -59,9 +58,10 @@ public void setupForTests() { // warm up the caches on every node for (NativeRolesStore rolesStore : internalCluster().getInstances(NativeRolesStore.class)) { - PlainActionFuture> future = new PlainActionFuture<>(); + PlainActionFuture future = new PlainActionFuture<>(); rolesStore.getRoleDescriptors(roles, future); assertThat(future.actionGet(), notNullValue()); + assertTrue(future.actionGet().isSuccess()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java index eecf3f0202f32..d28b8d38761de 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java @@ -18,11 +18,13 @@ import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -56,8 +58,8 @@ public void testReservedRoles() { doAnswer(invocation -> { Object[] args = invocation.getArguments(); assert args.length == 2; - ActionListener> listener = (ActionListener>) args[1]; - listener.onResponse(Collections.emptyList()); + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(RoleRetrievalResult.success(Collections.emptySet())); return null; }).when(rolesStore).getRoleDescriptors(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class)); @@ -100,8 +102,8 @@ public void testStoreRoles() { doAnswer(invocation -> { Object[] args = invocation.getArguments(); assert args.length == 2; - ActionListener> listener = (ActionListener>) args[1]; - listener.onResponse(storeRoleDescriptors); + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors))); return null; }).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class)); @@ -161,14 +163,14 @@ public void testGetAllOrMix() { Object[] args = invocation.getArguments(); assert args.length == 2; String[] requestedNames1 = (String[]) args[0]; - ActionListener> listener = (ActionListener>) args[1]; + ActionListener listener = (ActionListener) args[1]; if (requestedNames1.length == 0) { - listener.onResponse(storeRoleDescriptors); + listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors))); } else { List requestedNamesList = Arrays.asList(requestedNames1); - listener.onResponse(storeRoleDescriptors.stream() + listener.onResponse(RoleRetrievalResult.success(storeRoleDescriptors.stream() .filter(r -> requestedNamesList.contains(r.getName())) - .collect(Collectors.toList())); + .collect(Collectors.toSet()))); } return null; }).when(rolesStore).getRoleDescriptors(aryEq(specificStoreNames.toArray(Strings.EMPTY_ARRAY)), any(ActionListener.class)); @@ -216,7 +218,7 @@ public void testException() { doAnswer(invocation -> { Object[] args = invocation.getArguments(); assert args.length == 2; - ActionListener> listener = (ActionListener>) args[1]; + ActionListener listener = (ActionListener) args[1]; listener.onFailure(e); return null; }).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class)); 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 9f1490856d67b..b617789af4a6f 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 @@ -41,6 +41,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.IOException; @@ -204,8 +205,8 @@ public void testNegativeLookupsAreCached() { when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doAnswer((invocationOnMock) -> { - ActionListener> callback = (ActionListener>) invocationOnMock.getArguments()[1]; - callback.onResponse(Collections.emptySet()); + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.success(Collections.emptySet())); return null; }).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); @@ -242,9 +243,47 @@ public void testNegativeLookupsAreCached() { verify(reservedRolesStore, times(2)).roleDescriptors(); } verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore); + } + + public void testNegativeLookupsAreNotCachedWithFailures() { + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); + final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); - // force a cache clear + final CompositeRolesStore compositeRolesStore = + new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + final String roleName = randomAlphaOfLengthBetween(1, 10); + PlainActionFuture future = new PlainActionFuture<>(); + final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + compositeRolesStore.roles(Collections.singleton(roleName), fieldPermissionsCache, future); + final Role role = future.actionGet(); + assertEquals(Role.EMPTY, role); + verify(reservedRolesStore).roleDescriptors(); + verify(fileRolesStore).roleDescriptors(eq(Collections.singleton(roleName))); + verify(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); + + final int numberOfTimesToCall = scaledRandomIntBetween(0, 32); + final Set names = Collections.singleton(roleName); + for (int i = 0; i < numberOfTimesToCall; i++) { + future = new PlainActionFuture<>(); + compositeRolesStore.roles(names, fieldPermissionsCache, future); + future.actionGet(); + } + + verify(reservedRolesStore, times(numberOfTimesToCall + 1)).roleDescriptors(); + verify(fileRolesStore, times(numberOfTimesToCall + 1)).roleDescriptors(eq(Collections.singleton(roleName))); + verify(nativeRolesStore, times(numberOfTimesToCall + 1)).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); + verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore); } public void testCustomRolesProviders() { @@ -252,8 +291,8 @@ public void testCustomRolesProviders() { when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doAnswer((invocationOnMock) -> { - ActionListener> callback = (ActionListener>) invocationOnMock.getArguments()[1]; - callback.onResponse(Collections.emptySet()); + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.success(Collections.emptySet())); return null; }).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); @@ -266,7 +305,7 @@ public void testCustomRolesProviders() { IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build() }, null)); } - return descriptors; + return RoleRetrievalResult.success(descriptors); })); final InMemoryRolesProvider inMemoryProvider2 = spy(new InMemoryRolesProvider((roles) -> { @@ -285,7 +324,7 @@ public void testCustomRolesProviders() { IndicesPrivileges.builder().privileges("READ").indices("bar").grantedFields("*").build() }, null)); } - return descriptors; + return RoleRetrievalResult.success(descriptors); })); final CompositeRolesStore compositeRolesStore = @@ -471,8 +510,8 @@ public void testCustomRolesProviderFailures() throws Exception { when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doAnswer((invocationOnMock) -> { - ActionListener> callback = (ActionListener>) invocationOnMock.getArguments()[1]; - callback.onResponse(Collections.emptySet()); + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.success(Collections.emptySet())); return null; }).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); @@ -485,10 +524,10 @@ public void testCustomRolesProviderFailures() throws Exception { IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build() }, null)); } - return descriptors; + return RoleRetrievalResult.success(descriptors); }); - final BiConsumer, ActionListener>> failingProvider = + final BiConsumer, ActionListener> failingProvider = (roles, listener) -> listener.onFailure(new Exception("fake failure")); final CompositeRolesStore compositeRolesStore = @@ -513,8 +552,8 @@ public void testCustomRolesProvidersLicensing() { when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doAnswer((invocationOnMock) -> { - ActionListener> callback = (ActionListener>) invocationOnMock.getArguments()[1]; - callback.onResponse(Collections.emptySet()); + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.success(Collections.emptySet())); return null; }).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); @@ -527,7 +566,7 @@ public void testCustomRolesProvidersLicensing() { IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build() }, null)); } - return descriptors; + return RoleRetrievalResult.success(descriptors); }); UpdatableLicenseState xPackLicenseState = new UpdatableLicenseState(SECURITY_ENABLED_SETTINGS); @@ -647,15 +686,15 @@ public void invalidateAll() { assertEquals(2, numInvalidation.get()); } - private static class InMemoryRolesProvider implements BiConsumer, ActionListener>> { - private final Function, Set> roleDescriptorsFunc; + private static class InMemoryRolesProvider implements BiConsumer, ActionListener> { + private final Function, RoleRetrievalResult> roleDescriptorsFunc; - InMemoryRolesProvider(Function, Set> roleDescriptorsFunc) { + InMemoryRolesProvider(Function, RoleRetrievalResult> roleDescriptorsFunc) { this.roleDescriptorsFunc = roleDescriptorsFunc; } @Override - public void accept(Set roles, ActionListener> listener) { + public void accept(Set roles, ActionListener listener) { listener.onResponse(roleDescriptorsFunc.apply(roles)); } } diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/ExampleSecurityExtension.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/ExampleSecurityExtension.java index e426265c8a467..90b5eefcb56d4 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/ExampleSecurityExtension.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/ExampleSecurityExtension.java @@ -13,8 +13,8 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.Realm; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.SecurityExtension; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import java.security.AccessController; import java.security.PrivilegedAction; @@ -54,7 +54,7 @@ public AuthenticationFailureHandler getAuthenticationFailureHandler() { @Override - public List, ActionListener>>> + public List, ActionListener>> getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) { CustomInMemoryRolesProvider rp1 = new CustomInMemoryRolesProvider(settings, Collections.singletonMap(ROLE_A, "read")); Map roles = new HashMap<>(); diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/role/CustomInMemoryRolesProvider.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/role/CustomInMemoryRolesProvider.java index df9d3b5a6b875..0d5a71e6244b4 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/role/CustomInMemoryRolesProvider.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/role/CustomInMemoryRolesProvider.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import java.util.HashSet; import java.util.Map; @@ -21,7 +22,7 @@ */ public class CustomInMemoryRolesProvider extends AbstractComponent - implements BiConsumer, ActionListener>> { + implements BiConsumer, ActionListener> { public static final String INDEX = "foo"; public static final String ROLE_A = "roleA"; @@ -35,7 +36,7 @@ public CustomInMemoryRolesProvider(Settings settings, Map rolePe } @Override - public void accept(Set roles, ActionListener> listener) { + public void accept(Set roles, ActionListener listener) { Set roleDescriptors = new HashSet<>(); for (String role : roles) { if (rolePermissionSettings.containsKey(role)) { @@ -52,6 +53,6 @@ public void accept(Set roles, ActionListener> listen } } - listener.onResponse(roleDescriptors); + listener.onResponse(RoleRetrievalResult.success(roleDescriptors)); } }