Use RoleRetrievalResult for better caching (#34197)
Security caches the result of role lookups and negative lookups are cached indefinitely. In the case of transient failures this leads to a bad experience as the roles could truly exist. The CompositeRolesStore needs to know if a failure occurred in one of the roles stores in order to make the appropriate decision as it relates to caching. In order to provide this information to the CompositeRolesStore, the return type of methods to retrieve roles has changed to a new class, RoleRetrievalResult. This class provides the ability to pass back an exception to the roles store. This exception does not mean that a request should be failed but instead serves as a signal to the roles store that missing roles should not be cached and neither should the combined role if there are missing roles. As part of this, the negative lookup cache was also changed from an unbounded cache to a cache with a configurable limit. Relates #33205
This commit is contained in:
parent
a6d1cc6ca9
commit
0cd03d3581
|
@ -53,8 +53,7 @@ public class TransportXPackUsageAction extends TransportMasterNodeAction<XPackUs
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void masterOperation(XPackUsageRequest request, ClusterState state, ActionListener<XPackUsageResponse> listener)
|
||||
throws Exception {
|
||||
protected void masterOperation(XPackUsageRequest request, ClusterState state, ActionListener<XPackUsageResponse> listener) {
|
||||
final ActionListener<List<XPackFeatureSet.Usage>> usageActionListener = new ActionListener<List<Usage>>() {
|
||||
@Override
|
||||
public void onResponse(List<Usage> usages) {
|
||||
|
@ -73,7 +72,8 @@ public class TransportXPackUsageAction extends TransportMasterNodeAction<XPackUs
|
|||
@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 class TransportXPackUsageAction extends TransportMasterNodeAction<XPackUs
|
|||
};
|
||||
IteratingActionListener<List<XPackFeatureSet.Usage>, XPackFeatureSet> iteratingActionListener =
|
||||
new IteratingActionListener<>(usageActionListener, consumer, featureSets,
|
||||
threadPool.getThreadContext(), () -> {
|
||||
threadPool.getThreadContext(), (ignore) -> {
|
||||
final List<Usage> usageList = new ArrayList<>(featureSetUsages.length());
|
||||
for (int i = 0; i < featureSetUsages.length(); i++) {
|
||||
usageList.add(featureSetUsages.get(i));
|
||||
}
|
||||
return usageList;
|
||||
});
|
||||
}, (ignore) -> true);
|
||||
iteratingActionListener.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T, U> implements ActionListener<T>, R
|
|||
private final ActionListener<T> delegate;
|
||||
private final BiConsumer<U, ActionListener<T>> consumer;
|
||||
private final ThreadContext threadContext;
|
||||
private final Supplier<T> consumablesFinishedResponse;
|
||||
private final Function<T, T> finalResultFunction;
|
||||
private final Predicate<T> iterationPredicate;
|
||||
|
||||
private int position = 0;
|
||||
|
||||
|
@ -46,7 +49,7 @@ public final class IteratingActionListener<T, U> implements ActionListener<T>, R
|
|||
*/
|
||||
public IteratingActionListener(ActionListener<T> delegate, BiConsumer<U, ActionListener<T>> consumer, List<U> consumables,
|
||||
ThreadContext threadContext) {
|
||||
this(delegate, consumer, consumables, threadContext, null);
|
||||
this(delegate, consumer, consumables, threadContext, Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,18 +59,36 @@ public final class IteratingActionListener<T, U> implements ActionListener<T>, R
|
|||
* @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 consumablesFinishedResponse a supplier that maps the last consumable's response to a response
|
||||
* to be sent on the delegate listener, in case the last consumable returns a
|
||||
* {@code null} value, but the delegate listener should respond with some other value
|
||||
* (perhaps a concatenation of the results of all the consumables).
|
||||
* @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).
|
||||
*/
|
||||
public IteratingActionListener(ActionListener<T> delegate, BiConsumer<U, ActionListener<T>> consumer, List<U> consumables,
|
||||
ThreadContext threadContext, @Nullable Supplier<T> consumablesFinishedResponse) {
|
||||
ThreadContext threadContext, Function<T, T> 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<T> delegate, BiConsumer<U, ActionListener<T>> consumer, List<U> consumables,
|
||||
ThreadContext threadContext, Function<T, T> finalResultFunction,
|
||||
Predicate<T> 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 final class IteratingActionListener<T, U> implements ActionListener<T>, R
|
|||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler;
|
|||
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 @@ public interface SecurityExtension {
|
|||
* 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<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>>
|
||||
default List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>>
|
||||
getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.core.security.authz.store;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.collect.MapBuilder;
|
||||
import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
|
||||
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
|
||||
|
@ -21,9 +22,12 @@ import org.elasticsearch.xpack.core.watcher.watch.Watch;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ReservedRolesStore {
|
||||
public class ReservedRolesStore implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
|
||||
|
||||
public static final RoleDescriptor SUPERUSER_ROLE_DESCRIPTOR = new RoleDescriptor("superuser",
|
||||
new String[] { "all" },
|
||||
|
@ -165,4 +169,18 @@ public class ReservedRolesStore {
|
|||
public static Set<String> names() {
|
||||
return RESERVED_ROLES.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> roleNames, ActionListener<RoleRetrievalResult> listener) {
|
||||
final Set<RoleDescriptor> descriptors = roleNames.stream()
|
||||
.map(RESERVED_ROLES::get)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
listener.onResponse(RoleRetrievalResult.success(descriptors));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "reserved roles store";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.common.Nullable;
|
||||
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<RoleDescriptor> descriptors;
|
||||
|
||||
@Nullable
|
||||
private final Exception failure;
|
||||
|
||||
private RoleRetrievalResult(Set<RoleDescriptor> descriptors, @Nullable 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<RoleDescriptor> getDescriptors() {
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the failure or {@code null} if retrieval succeeded
|
||||
*/
|
||||
@Nullable
|
||||
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<RoleDescriptor> 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);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ package org.elasticsearch.xpack.core.common;
|
|||
|
||||
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.Collections;
|
|||
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 class IteratingActionListenerTests extends ESTestCase {
|
|||
assertEquals(numberOfIterations, iterations.get());
|
||||
assertTrue(onFailureCalled.get());
|
||||
}
|
||||
|
||||
public void testFunctionApplied() {
|
||||
final int numberOfItems = scaledRandomIntBetween(2, 32);
|
||||
final int numberOfIterations = scaledRandomIntBetween(1, numberOfItems);
|
||||
List<Object> items = new ArrayList<>(numberOfItems);
|
||||
for (int i = 0; i < numberOfItems; i++) {
|
||||
items.add(new Object());
|
||||
}
|
||||
|
||||
final AtomicInteger iterations = new AtomicInteger(0);
|
||||
final Predicate<Object> iterationPredicate = object -> {
|
||||
final int current = iterations.incrementAndGet();
|
||||
return current != numberOfIterations;
|
||||
};
|
||||
final BiConsumer<Object, ActionListener<Object>> consumer = (listValue, listener) -> {
|
||||
listener.onResponse(items.get(iterations.get()));
|
||||
};
|
||||
|
||||
final AtomicReference<Object> originalObject = new AtomicReference<>();
|
||||
final AtomicReference<Object> result = new AtomicReference<>();
|
||||
final Function<Object, Object> responseFunction = object -> {
|
||||
originalObject.set(object);
|
||||
Object randomResult;
|
||||
do {
|
||||
randomResult = randomFrom(items);
|
||||
} while (randomResult == object);
|
||||
result.set(randomResult);
|
||||
return randomResult;
|
||||
};
|
||||
|
||||
IteratingActionListener<Object, Object> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,6 @@ import org.elasticsearch.xpack.core.security.authc.Realm;
|
|||
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.CompositeRolesStore;
|
|||
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 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
|
|||
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<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> rolesProviders = new ArrayList<>();
|
||||
List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> rolesProviders = new ArrayList<>();
|
||||
for (SecurityExtension extension : securityExtensions) {
|
||||
rolesProviders.addAll(extension.getRolesProviders(settings, resourceWatcherService));
|
||||
}
|
||||
|
@ -610,7 +610,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
|
|||
AuthenticationService.addSettings(settingsList);
|
||||
AuthorizationService.addSettings(settingsList);
|
||||
Automatons.addSettings(settingsList);
|
||||
settingsList.add(CompositeRolesStore.CACHE_SIZE_SETTING);
|
||||
settingsList.addAll(CompositeRolesStore.getSettings());
|
||||
settingsList.add(FieldPermissionsCache.CACHE_SIZE_SETTING);
|
||||
settingsList.add(TokenService.TOKEN_EXPIRATION);
|
||||
settingsList.add(TokenService.DELETE_INTERVAL);
|
||||
|
|
|
@ -20,7 +20,9 @@ import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
|
|||
import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequest, GetRolesResponse> {
|
||||
|
||||
|
@ -41,7 +43,7 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
|
|||
protected void doExecute(Task task, final GetRolesRequest request, final ActionListener<GetRolesResponse> listener) {
|
||||
final String[] requestedRoles = request.names();
|
||||
final boolean specificRolesRequested = requestedRoles != null && requestedRoles.length > 0;
|
||||
final List<String> rolesToSearchFor = new ArrayList<>();
|
||||
final Set<String> rolesToSearchFor = new HashSet<>();
|
||||
final List<RoleDescriptor> roles = new ArrayList<>();
|
||||
|
||||
if (specificRolesRequested) {
|
||||
|
@ -66,11 +68,14 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
|
|||
// specific roles were requested but they were built in only, no need to hit the store
|
||||
listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
|
||||
} else {
|
||||
String[] roleNames = rolesToSearchFor.toArray(new String[rolesToSearchFor.size()]);
|
||||
nativeRolesStore.getRoleDescriptors(roleNames, ActionListener.wrap((foundRoles) -> {
|
||||
roles.addAll(foundRoles);
|
||||
listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
|
||||
}, listener::onFailure));
|
||||
nativeRolesStore.getRoleDescriptors(rolesToSearchFor, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authz.store;
|
|||
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.ContextPreservingActionListener;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
|
@ -17,7 +18,6 @@ import org.elasticsearch.common.component.AbstractComponent;
|
|||
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 +34,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterP
|
|||
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 +52,11 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||
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,29 +79,30 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
writeLock = new ReleasableLock(iterationLock.writeLock());
|
||||
}
|
||||
|
||||
public static final Setting<Integer> CACHE_SIZE_SETTING =
|
||||
Setting.intSetting(setting("authz.store.roles.cache.max_size"), 10000, Property.NodeScope);
|
||||
private static final Setting<Integer> CACHE_SIZE_SETTING =
|
||||
Setting.intSetting("xpack.security.authz.store.roles.cache.max_size", 10000, Property.NodeScope);
|
||||
private static final Setting<Integer> 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;
|
||||
private final ReservedRolesStore reservedRolesStore;
|
||||
private final NativePrivilegeStore privilegeStore;
|
||||
private final XPackLicenseState licenseState;
|
||||
private final Cache<Set<String>, Role> roleCache;
|
||||
private final Set<String> negativeLookupCache;
|
||||
private final Cache<String, Boolean> negativeLookupCache;
|
||||
private final ThreadContext threadContext;
|
||||
private final AtomicLong numInvalidation = new AtomicLong();
|
||||
private final List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> customRolesProviders;
|
||||
private final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> builtInRoleProviders;
|
||||
private final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> allRoleProviders;
|
||||
|
||||
public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore,
|
||||
ReservedRolesStore reservedRolesStore, NativePrivilegeStore privilegeStore,
|
||||
List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> rolesProviders,
|
||||
List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> rolesProviders,
|
||||
ThreadContext threadContext, XPackLicenseState licenseState) {
|
||||
super(settings);
|
||||
this.fileRolesStore = fileRolesStore;
|
||||
fileRolesStore.addListener(this::invalidate);
|
||||
this.nativeRolesStore = nativeRolesStore;
|
||||
this.reservedRolesStore = reservedRolesStore;
|
||||
this.privilegeStore = privilegeStore;
|
||||
this.licenseState = licenseState;
|
||||
CacheBuilder<Set<String>, Role> builder = CacheBuilder.builder();
|
||||
|
@ -109,8 +112,22 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
this.roleCache = builder.build();
|
||||
this.threadContext = threadContext;
|
||||
this.negativeLookupCache = ConcurrentCollections.newConcurrentSet();
|
||||
this.customRolesProviders = Collections.unmodifiableList(rolesProviders);
|
||||
CacheBuilder<String, Boolean> nlcBuilder = CacheBuilder.builder();
|
||||
final int nlcCacheSize = NEGATIVE_LOOKUP_CACHE_SIZE_SETTING.get(settings);
|
||||
if (nlcCacheSize >= 0) {
|
||||
nlcBuilder.setMaximumWeight(nlcCacheSize);
|
||||
}
|
||||
this.negativeLookupCache = nlcBuilder.build();
|
||||
this.builtInRoleProviders = Collections.unmodifiableList(Arrays.asList(reservedRolesStore, fileRolesStore, nativeRolesStore));
|
||||
if (rolesProviders.isEmpty()) {
|
||||
this.allRoleProviders = this.builtInRoleProviders;
|
||||
} else {
|
||||
List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> allList =
|
||||
new ArrayList<>(builtInRoleProviders.size() + rolesProviders.size());
|
||||
allList.addAll(builtInRoleProviders);
|
||||
allList.addAll(rolesProviders);
|
||||
this.allRoleProviders = Collections.unmodifiableList(allList);
|
||||
}
|
||||
}
|
||||
|
||||
public void roles(Set<String> roleNames, FieldPermissionsCache fieldPermissionsCache, ActionListener<Role> roleActionListener) {
|
||||
|
@ -120,18 +137,23 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
} 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<RoleDescriptor> 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) {
|
||||
if (role != null && rolesRetrievalResult.isSuccess()) {
|
||||
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
|
||||
|
@ -144,6 +166,10 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
roleCache.computeIfAbsent(roleNames, (s) -> role);
|
||||
}
|
||||
}
|
||||
|
||||
for (String missingRole : rolesRetrievalResult.getMissingRoles()) {
|
||||
negativeLookupCache.computeIfAbsent(missingRole, s -> Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
roleActionListener.onResponse(role);
|
||||
}, roleActionListener::onFailure));
|
||||
|
@ -152,97 +178,56 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private void roleDescriptors(Set<String> roleNames, ActionListener<Set<RoleDescriptor>> roleDescriptorActionListener) {
|
||||
private void roleDescriptors(Set<String> roleNames, ActionListener<RolesRetrievalResult> rolesResultListener) {
|
||||
final Set<String> 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 Set<RoleDescriptor> builtInRoleDescriptors = getBuiltInRoleDescriptors(filteredRoleNames);
|
||||
Set<String> remainingRoleNames = difference(filteredRoleNames, builtInRoleDescriptors);
|
||||
if (remainingRoleNames.isEmpty()) {
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
} 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);
|
||||
}));
|
||||
}
|
||||
|
||||
loadRoleDescriptorsAsync(filteredRoleNames, rolesResultListener);
|
||||
}
|
||||
|
||||
private void callCustomRoleProvidersIfEnabled(Set<RoleDescriptor> builtInRoleDescriptors, Set<String> filteredRoleNames,
|
||||
ActionListener<Set<RoleDescriptor>> roleDescriptorActionListener) {
|
||||
if (builtInRoleDescriptors.size() != filteredRoleNames.size()) {
|
||||
final Set<String> 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();
|
||||
} else {
|
||||
logger.debug(() ->
|
||||
new ParameterizedMessage("Requested roles [{}] do not exist", Strings.collectionToCommaDelimitedString(missing)));
|
||||
negativeLookupCache.addAll(missing);
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
}
|
||||
} else {
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
}
|
||||
}
|
||||
private void loadRoleDescriptorsAsync(Set<String> roleNames, ActionListener<RolesRetrievalResult> listener) {
|
||||
final RolesRetrievalResult rolesResult = new RolesRetrievalResult();
|
||||
final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> asyncRoleProviders =
|
||||
licenseState.isCustomRoleProvidersAllowed() ? allRoleProviders : builtInRoleProviders;
|
||||
|
||||
private Set<RoleDescriptor> getBuiltInRoleDescriptors(Set<String> roleNames) {
|
||||
final Set<RoleDescriptor> descriptors = reservedRolesStore.roleDescriptors().stream()
|
||||
.filter((rd) -> roleNames.contains(rd.getName()))
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
if (descriptors.size() > 0) {
|
||||
logger.debug(() -> new ParameterizedMessage("Roles [{}] are builtin roles", names(descriptors)));
|
||||
}
|
||||
final Set<String> difference = difference(roleNames, descriptors);
|
||||
if (difference.isEmpty() == false) {
|
||||
final Set<RoleDescriptor> fileRoles = fileRolesStore.roleDescriptors(difference);
|
||||
logger.debug(() ->
|
||||
new ParameterizedMessage("Roles [{}] were resolved from [{}]", names(fileRoles), fileRolesStore.getFile()));
|
||||
descriptors.addAll(fileRoles);
|
||||
}
|
||||
final ActionListener<RoleRetrievalResult> descriptorsListener =
|
||||
ContextPreservingActionListener.wrapPreservingContext(ActionListener.wrap(ignore -> {
|
||||
rolesResult.setMissingRoles(roleNames);
|
||||
listener.onResponse(rolesResult);
|
||||
}, listener::onFailure), threadContext);
|
||||
|
||||
return descriptors;
|
||||
final Predicate<RoleRetrievalResult> iterationPredicate = result -> roleNames.isEmpty() == false;
|
||||
new IteratingActionListener<>(descriptorsListener, (rolesProvider, providerListener) -> {
|
||||
// try to resolve descriptors with role provider
|
||||
rolesProvider.accept(roleNames, ActionListener.wrap(result -> {
|
||||
if (result.isSuccess()) {
|
||||
logger.debug(() -> new ParameterizedMessage("Roles [{}] were resolved by [{}]",
|
||||
names(result.getDescriptors()), rolesProvider));
|
||||
final Set<RoleDescriptor> resolvedDescriptors = result.getDescriptors();
|
||||
rolesResult.addDescriptors(resolvedDescriptors);
|
||||
// remove resolved descriptors from the set of roles still needed to be resolved
|
||||
for (RoleDescriptor descriptor : resolvedDescriptors) {
|
||||
roleNames.remove(descriptor.getName());
|
||||
}
|
||||
} else {
|
||||
logger.warn(new ParameterizedMessage("role retrieval failed from [{}]", rolesProvider), result.getFailure());
|
||||
rolesResult.setFailure();
|
||||
}
|
||||
providerListener.onResponse(result);
|
||||
}, providerListener::onFailure));
|
||||
}, asyncRoleProviders, threadContext, Function.identity(), iterationPredicate).run();
|
||||
}
|
||||
|
||||
private String names(Collection<RoleDescriptor> descriptors) {
|
||||
return descriptors.stream().map(RoleDescriptor::getName).collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private Set<String> difference(Set<String> roleNames, Set<RoleDescriptor> descriptors) {
|
||||
Set<String> foundNames = descriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toSet());
|
||||
return Sets.difference(roleNames, foundNames);
|
||||
}
|
||||
|
||||
public static void buildRoleFromDescriptors(Collection<RoleDescriptor> roleDescriptors, FieldPermissionsCache fieldPermissionsCache,
|
||||
NativePrivilegeStore privilegeStore, ActionListener<Role> listener) {
|
||||
if (roleDescriptors.isEmpty()) {
|
||||
|
@ -332,7 +317,7 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
|
||||
public void invalidateAll() {
|
||||
numInvalidation.incrementAndGet();
|
||||
negativeLookupCache.clear();
|
||||
negativeLookupCache.invalidateAll();
|
||||
try (ReleasableLock ignored = readLock.acquire()) {
|
||||
roleCache.invalidateAll();
|
||||
}
|
||||
|
@ -351,7 +336,7 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
negativeLookupCache.remove(role);
|
||||
negativeLookupCache.invalidate(role);
|
||||
}
|
||||
|
||||
public void invalidate(Set<String> roles) {
|
||||
|
@ -368,7 +353,7 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
|
||||
negativeLookupCache.removeAll(roles);
|
||||
roles.forEach(negativeLookupCache::invalidate);
|
||||
}
|
||||
|
||||
public void usageStats(ActionListener<Map<String, Object>> listener) {
|
||||
|
@ -387,6 +372,11 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
|
||||
// pkg - private for testing
|
||||
boolean isValueInNegativeLookupCache(String key) {
|
||||
return negativeLookupCache.get(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutable class that can be used to represent the combination of one or more {@link IndicesPrivileges}
|
||||
*/
|
||||
|
@ -421,4 +411,39 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RolesRetrievalResult {
|
||||
|
||||
private final Set<RoleDescriptor> roleDescriptors = new HashSet<>();
|
||||
private Set<String> missingRoles = Collections.emptySet();
|
||||
private boolean success = true;
|
||||
|
||||
private void addDescriptors(Set<RoleDescriptor> descriptors) {
|
||||
roleDescriptors.addAll(descriptors);
|
||||
}
|
||||
|
||||
private Set<RoleDescriptor> getRoleDescriptors() {
|
||||
return roleDescriptors;
|
||||
}
|
||||
|
||||
private void setFailure() {
|
||||
success = false;
|
||||
}
|
||||
|
||||
private boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
private void setMissingRoles(Set<String> missingRoles) {
|
||||
this.missingRoles = missingRoles;
|
||||
}
|
||||
|
||||
private Set<String> getMissingRoles() {
|
||||
return missingRoles;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Setting<?>> getSettings() {
|
||||
return Arrays.asList(CACHE_SIZE_SETTING, NEGATIVE_LOOKUP_CACHE_SIZE_SETTING);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.apache.logging.log4j.Logger;
|
|||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.apache.logging.log4j.util.Supplier;
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
@ -27,6 +28,7 @@ import org.elasticsearch.xpack.core.XPackSettings;
|
|||
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.ReservedRolesStore;
|
||||
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
|
||||
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
|
||||
import org.elasticsearch.xpack.core.security.support.Validation;
|
||||
|
||||
|
@ -42,6 +44,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -49,7 +52,7 @@ import java.util.stream.Collectors;
|
|||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
public class FileRolesStore extends AbstractComponent {
|
||||
public class FileRolesStore extends AbstractComponent implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
|
||||
|
||||
private static final Pattern IN_SEGMENT_LINE = Pattern.compile("^\\s+.+");
|
||||
private static final Pattern SKIP_LINE = Pattern.compile("(^#.*|^\\s*)");
|
||||
|
@ -79,7 +82,13 @@ public class FileRolesStore extends AbstractComponent {
|
|||
permissions = parseFile(file, logger, settings, licenseState);
|
||||
}
|
||||
|
||||
public Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> names, ActionListener<RoleRetrievalResult> listener) {
|
||||
listener.onResponse(RoleRetrievalResult.success(roleDescriptors(names)));
|
||||
}
|
||||
|
||||
Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
|
||||
final Map<String, RoleDescriptor> localPermissions = permissions;
|
||||
Set<RoleDescriptor> descriptors = new HashSet<>();
|
||||
roleNames.forEach((name) -> {
|
||||
|
@ -129,6 +138,11 @@ public class FileRolesStore extends AbstractComponent {
|
|||
return permissions.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "file roles store (" + file + ")";
|
||||
}
|
||||
|
||||
public static Path resolveFile(Environment env) {
|
||||
return XPackPlugin.resolveConfigFile(env, "roles.yml");
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import org.elasticsearch.action.search.MultiSearchResponse;
|
|||
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.DeleteRoleRequest;
|
|||
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,9 +52,12 @@ import java.util.Arrays;
|
|||
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;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
|
@ -75,7 +78,7 @@ import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECU
|
|||
*
|
||||
* No caching is done by this class, it is handled at a higher level
|
||||
*/
|
||||
public class NativeRolesStore extends AbstractComponent {
|
||||
public class NativeRolesStore extends AbstractComponent implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
|
||||
|
||||
// these are no longer used, but leave them around for users upgrading
|
||||
private static final Setting<Integer> CACHE_SIZE_SETTING =
|
||||
|
@ -98,24 +101,27 @@ public class NativeRolesStore extends AbstractComponent {
|
|||
this.securityIndex = securityIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> names, ActionListener<RoleRetrievalResult> listener) {
|
||||
getRoleDescriptors(names, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of roles, if rolesToGet is null or empty, fetch all roles
|
||||
*/
|
||||
public void getRoleDescriptors(String[] names, final ActionListener<Collection<RoleDescriptor>> listener) {
|
||||
public void getRoleDescriptors(Set<String> names, final ActionListener<RoleRetrievalResult> listener) {
|
||||
if (securityIndex.indexExists() == false) {
|
||||
// TODO remove this short circuiting and fix tests that fail without this!
|
||||
listener.onResponse(Collections.emptyList());
|
||||
} 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));
|
||||
listener.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
} else if (names != null && names.size() == 1) {
|
||||
getRoleDescriptor(Objects.requireNonNull(names.iterator().next()), listener);
|
||||
} else {
|
||||
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> {
|
||||
QueryBuilder query;
|
||||
if (names == null || names.length == 0) {
|
||||
if (names == null || names.isEmpty()) {
|
||||
query = QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE);
|
||||
} else {
|
||||
final String[] roleNames = Arrays.stream(names).map(s -> getIdForUser(s)).toArray(String[]::new);
|
||||
final String[] roleNames = names.stream().map(NativeRolesStore::getIdForUser).toArray(String[]::new);
|
||||
query = QueryBuilders.boolQuery().filter(QueryBuilders.idsQuery(ROLE_DOC_TYPE).addIds(roleNames));
|
||||
}
|
||||
final Supplier<ThreadContext.StoredContext> supplier = client.threadPool().getThreadContext().newRestorableContext(false);
|
||||
|
@ -127,7 +133,10 @@ public class NativeRolesStore extends AbstractComponent {
|
|||
.setFetchSource(true)
|
||||
.request();
|
||||
request.indicesOptions().ignoreUnavailable();
|
||||
ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener),
|
||||
final ActionListener<Collection<RoleDescriptor>> 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 +270,28 @@ public class NativeRolesStore extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private void getRoleDescriptor(final String roleId, ActionListener<RoleDescriptor> roleActionListener) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "native roles store";
|
||||
}
|
||||
|
||||
private void getRoleDescriptor(final String roleId, ActionListener<RoleRetrievalResult> 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<GetResponse>() {
|
||||
@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));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import org.elasticsearch.test.NativeRealmIntegTestCase;
|
|||
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,8 +19,9 @@ import org.junit.Before;
|
|||
import org.junit.BeforeClass;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
||||
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.NONE;
|
||||
|
@ -57,11 +58,13 @@ public class ClearRolesCacheTests extends NativeRealmIntegTestCase {
|
|||
|
||||
ensureGreen(SecurityIndexManager.SECURITY_INDEX_NAME);
|
||||
|
||||
final Set<String> rolesSet = new HashSet<>(Arrays.asList(roles));
|
||||
// warm up the caches on every node
|
||||
for (NativeRolesStore rolesStore : internalCluster().getInstances(NativeRolesStore.class)) {
|
||||
PlainActionFuture<Collection<RoleDescriptor>> future = new PlainActionFuture<>();
|
||||
rolesStore.getRoleDescriptors(roles, future);
|
||||
PlainActionFuture<RoleRetrievalResult> future = new PlainActionFuture<>();
|
||||
rolesStore.getRoleDescriptors(rolesSet, future);
|
||||
assertThat(future.actionGet(), notNullValue());
|
||||
assertTrue(future.actionGet().isSuccess());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,12 +18,15 @@ import org.elasticsearch.xpack.core.security.action.role.GetRolesRequest;
|
|||
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.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -31,8 +34,8 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
|||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.AdditionalMatchers.aryEq;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
@ -56,10 +59,10 @@ public class TransportGetRolesActionTests extends ESTestCase {
|
|||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
assert args.length == 2;
|
||||
ActionListener<List<RoleDescriptor>> listener = (ActionListener<List<RoleDescriptor>>) args[1];
|
||||
listener.onResponse(Collections.emptyList());
|
||||
ActionListener<RoleRetrievalResult> listener = (ActionListener<RoleRetrievalResult>) args[1];
|
||||
listener.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(rolesStore).getRoleDescriptors(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class));
|
||||
}).when(rolesStore).getRoleDescriptors(eq(new HashSet<>()), any(ActionListener.class));
|
||||
|
||||
GetRolesRequest request = new GetRolesRequest();
|
||||
request.names(names.toArray(Strings.EMPTY_ARRAY));
|
||||
|
@ -100,10 +103,10 @@ public class TransportGetRolesActionTests extends ESTestCase {
|
|||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
assert args.length == 2;
|
||||
ActionListener<List<RoleDescriptor>> listener = (ActionListener<List<RoleDescriptor>>) args[1];
|
||||
listener.onResponse(storeRoleDescriptors);
|
||||
ActionListener<RoleRetrievalResult> listener = (ActionListener<RoleRetrievalResult>) args[1];
|
||||
listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors)));
|
||||
return null;
|
||||
}).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class));
|
||||
}).when(rolesStore).getRoleDescriptors(eq(new HashSet<>(Arrays.asList(request.names()))), any(ActionListener.class));
|
||||
|
||||
final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
|
||||
final AtomicReference<GetRolesResponse> responseRef = new AtomicReference<>();
|
||||
|
@ -160,18 +163,17 @@ public class TransportGetRolesActionTests extends ESTestCase {
|
|||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
assert args.length == 2;
|
||||
String[] requestedNames1 = (String[]) args[0];
|
||||
ActionListener<List<RoleDescriptor>> listener = (ActionListener<List<RoleDescriptor>>) args[1];
|
||||
if (requestedNames1.length == 0) {
|
||||
listener.onResponse(storeRoleDescriptors);
|
||||
Set<String> requestedNames1 = (Set<String>) args[0];
|
||||
ActionListener<RoleRetrievalResult> listener = (ActionListener<RoleRetrievalResult>) args[1];
|
||||
if (requestedNames1.size() == 0) {
|
||||
listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors)));
|
||||
} else {
|
||||
List<String> requestedNamesList = Arrays.asList(requestedNames1);
|
||||
listener.onResponse(storeRoleDescriptors.stream()
|
||||
.filter(r -> requestedNamesList.contains(r.getName()))
|
||||
.collect(Collectors.toList()));
|
||||
listener.onResponse(RoleRetrievalResult.success(storeRoleDescriptors.stream()
|
||||
.filter(r -> requestedNames1.contains(r.getName()))
|
||||
.collect(Collectors.toSet())));
|
||||
}
|
||||
return null;
|
||||
}).when(rolesStore).getRoleDescriptors(aryEq(specificStoreNames.toArray(Strings.EMPTY_ARRAY)), any(ActionListener.class));
|
||||
}).when(rolesStore).getRoleDescriptors(eq(new HashSet<>(specificStoreNames)), any(ActionListener.class));
|
||||
|
||||
final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
|
||||
final AtomicReference<GetRolesResponse> responseRef = new AtomicReference<>();
|
||||
|
@ -194,10 +196,10 @@ public class TransportGetRolesActionTests extends ESTestCase {
|
|||
assertThat(retrievedRoleNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY)));
|
||||
|
||||
if (all) {
|
||||
verify(rolesStore, times(1)).getRoleDescriptors(aryEq(Strings.EMPTY_ARRAY), any(ActionListener.class));
|
||||
verify(rolesStore, times(1)).getRoleDescriptors(eq(new HashSet<>()), any(ActionListener.class));
|
||||
} else {
|
||||
verify(rolesStore, times(1))
|
||||
.getRoleDescriptors(aryEq(specificStoreNames.toArray(Strings.EMPTY_ARRAY)), any(ActionListener.class));
|
||||
.getRoleDescriptors(eq(new HashSet<>(specificStoreNames)), any(ActionListener.class));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,10 +218,10 @@ public class TransportGetRolesActionTests extends ESTestCase {
|
|||
doAnswer(invocation -> {
|
||||
Object[] args = invocation.getArguments();
|
||||
assert args.length == 2;
|
||||
ActionListener<List<RoleDescriptor>> listener = (ActionListener<List<RoleDescriptor>>) args[1];
|
||||
ActionListener<RoleRetrievalResult> listener = (ActionListener<RoleRetrievalResult>) args[1];
|
||||
listener.onFailure(e);
|
||||
return null;
|
||||
}).when(rolesStore).getRoleDescriptors(aryEq(request.names()), any(ActionListener.class));
|
||||
}).when(rolesStore).getRoleDescriptors(eq(new HashSet<>(Arrays.asList(request.names()))), any(ActionListener.class));
|
||||
|
||||
final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
|
||||
final AtomicReference<GetRolesResponse> responseRef = new AtomicReference<>();
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
|
|||
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;
|
||||
|
@ -66,6 +67,7 @@ import static org.mockito.Matchers.anySetOf;
|
|||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isA;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -112,12 +114,18 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
.build()
|
||||
}, null);
|
||||
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class);
|
||||
doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole));
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class),
|
||||
mock(ReservedRolesStore.class), mock(NativePrivilegeStore.class), Collections.emptyList(),
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore,
|
||||
reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(),
|
||||
new ThreadContext(Settings.EMPTY), licenseState);
|
||||
|
||||
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
|
@ -173,12 +181,17 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
.build()
|
||||
}, null);
|
||||
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class);
|
||||
doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole));
|
||||
when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole));
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class),
|
||||
mock(ReservedRolesStore.class), mock(NativePrivilegeStore.class), Collections.emptyList(),
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore,
|
||||
reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(),
|
||||
new ThreadContext(Settings.EMPTY), licenseState);
|
||||
|
||||
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
|
@ -201,13 +214,15 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
|
||||
public void testNegativeLookupsAreCached() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
|
@ -222,9 +237,11 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
compositeRolesStore.roles(Collections.singleton(roleName), fieldPermissionsCache, future);
|
||||
final Role role = future.actionGet();
|
||||
assertEquals(Role.EMPTY, role);
|
||||
verify(reservedRolesStore).roleDescriptors();
|
||||
verify(reservedRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).roleDescriptors(eq(Collections.singleton(roleName)));
|
||||
verify(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
verify(nativeRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
|
||||
final int numberOfTimesToCall = scaledRandomIntBetween(0, 32);
|
||||
final boolean getSuperuserRole = randomBoolean()
|
||||
|
@ -239,23 +256,107 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
|
||||
if (getSuperuserRole && numberOfTimesToCall > 0) {
|
||||
// the superuser role was requested so we get the role descriptors again
|
||||
verify(reservedRolesStore, times(2)).roleDescriptors();
|
||||
verify(reservedRolesStore, times(2)).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
}
|
||||
verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore);
|
||||
}
|
||||
|
||||
// force a cache clear
|
||||
public void testNegativeLookupsCacheDisabled() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
final Settings settings = Settings.builder().put(SECURITY_ENABLED_SETTINGS)
|
||||
.put("xpack.security.authz.store.roles.negative_lookup_cache.max_size", 0)
|
||||
.build();
|
||||
final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore,
|
||||
reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(settings),
|
||||
new XPackLicenseState(settings));
|
||||
verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
|
||||
|
||||
final String roleName = randomAlphaOfLengthBetween(1, 10);
|
||||
PlainActionFuture<Role> 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).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).roleDescriptors(eq(Collections.singleton(roleName)));
|
||||
verify(nativeRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
|
||||
assertFalse(compositeRolesStore.isValueInNegativeLookupCache(roleName));
|
||||
verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore);
|
||||
}
|
||||
|
||||
public void testNegativeLookupsAreNotCachedWithFailures() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!")));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
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<Role> 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).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore).roleDescriptors(eq(Collections.singleton(roleName)));
|
||||
verify(nativeRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
|
||||
final int numberOfTimesToCall = scaledRandomIntBetween(0, 32);
|
||||
final Set<String> names = Collections.singleton(roleName);
|
||||
for (int i = 0; i < numberOfTimesToCall; i++) {
|
||||
future = new PlainActionFuture<>();
|
||||
compositeRolesStore.roles(names, fieldPermissionsCache, future);
|
||||
future.actionGet();
|
||||
}
|
||||
|
||||
assertFalse(compositeRolesStore.isValueInNegativeLookupCache(roleName));
|
||||
verify(reservedRolesStore, times(numberOfTimesToCall + 1)).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore, times(numberOfTimesToCall + 1)).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(fileRolesStore, times(numberOfTimesToCall + 1)).roleDescriptors(eq(Collections.singleton(roleName)));
|
||||
verify(nativeRolesStore, times(numberOfTimesToCall + 1)).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(nativeRolesStore, times(numberOfTimesToCall + 1)).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore);
|
||||
}
|
||||
|
||||
public void testCustomRolesProviders() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider1 = spy(new InMemoryRolesProvider((roles) -> {
|
||||
|
@ -266,7 +367,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
return RoleRetrievalResult.success(descriptors);
|
||||
}));
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider2 = spy(new InMemoryRolesProvider((roles) -> {
|
||||
|
@ -285,7 +386,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
IndicesPrivileges.builder().privileges("READ").indices("bar").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
return RoleRetrievalResult.success(descriptors);
|
||||
}));
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
|
@ -468,13 +569,15 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
|
||||
public void testCustomRolesProviderFailures() throws Exception {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = new ReservedRolesStore();
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider1 = new InMemoryRolesProvider((roles) -> {
|
||||
|
@ -485,10 +588,10 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
return RoleRetrievalResult.success(descriptors);
|
||||
});
|
||||
|
||||
final BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> failingProvider =
|
||||
final BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> failingProvider =
|
||||
(roles, listener) -> listener.onFailure(new Exception("fake failure"));
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
|
@ -510,13 +613,15 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
|
||||
public void testCustomRolesProvidersLicensing() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
ActionListener<RoleRetrievalResult> callback = (ActionListener<RoleRetrievalResult>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(RoleRetrievalResult.success(Collections.emptySet()));
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = new ReservedRolesStore();
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider = new InMemoryRolesProvider((roles) -> {
|
||||
|
@ -527,7 +632,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
return RoleRetrievalResult.success(descriptors);
|
||||
});
|
||||
|
||||
UpdatableLicenseState xPackLicenseState = new UpdatableLicenseState(SECURITY_ENABLED_SETTINGS);
|
||||
|
@ -580,8 +685,14 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
public void testCacheClearOnIndexHealthChange() {
|
||||
final AtomicInteger numInvalidation = new AtomicInteger(0);
|
||||
|
||||
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class);
|
||||
doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(
|
||||
Settings.EMPTY, mock(FileRolesStore.class), mock(NativeRolesStore.class), mock(ReservedRolesStore.class),
|
||||
Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore,
|
||||
mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY),
|
||||
new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) {
|
||||
@Override
|
||||
|
@ -626,8 +737,14 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
public void testCacheClearOnIndexOutOfDateChange() {
|
||||
final AtomicInteger numInvalidation = new AtomicInteger(0);
|
||||
|
||||
FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class);
|
||||
doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class));
|
||||
CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS,
|
||||
mock(FileRolesStore.class), mock(NativeRolesStore.class), mock(ReservedRolesStore.class),
|
||||
fileRolesStore, nativeRolesStore, reservedRolesStore,
|
||||
mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
|
||||
new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) {
|
||||
@Override
|
||||
|
@ -647,15 +764,15 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
assertEquals(2, numInvalidation.get());
|
||||
}
|
||||
|
||||
private static class InMemoryRolesProvider implements BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> {
|
||||
private final Function<Set<String>, Set<RoleDescriptor>> roleDescriptorsFunc;
|
||||
private static class InMemoryRolesProvider implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
|
||||
private final Function<Set<String>, RoleRetrievalResult> roleDescriptorsFunc;
|
||||
|
||||
InMemoryRolesProvider(Function<Set<String>, Set<RoleDescriptor>> roleDescriptorsFunc) {
|
||||
InMemoryRolesProvider(Function<Set<String>, RoleRetrievalResult> roleDescriptorsFunc) {
|
||||
this.roleDescriptorsFunc = roleDescriptorsFunc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> roles, ActionListener<Set<RoleDescriptor>> listener) {
|
||||
public void accept(Set<String> roles, ActionListener<RoleRetrievalResult> listener) {
|
||||
listener.onResponse(roleDescriptorsFunc.apply(roles));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import org.elasticsearch.example.role.CustomInMemoryRolesProvider;
|
|||
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 class ExampleSecurityExtension implements SecurityExtension {
|
|||
|
||||
|
||||
@Override
|
||||
public List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>>
|
||||
public List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>>
|
||||
getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) {
|
||||
CustomInMemoryRolesProvider rp1 = new CustomInMemoryRolesProvider(settings, Collections.singletonMap(ROLE_A, "read"));
|
||||
Map<String, String> roles = new HashMap<>();
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.elasticsearch.action.ActionListener;
|
|||
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 @@ import java.util.function.BiConsumer;
|
|||
*/
|
||||
public class CustomInMemoryRolesProvider
|
||||
extends AbstractComponent
|
||||
implements BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> {
|
||||
implements BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>> {
|
||||
|
||||
public static final String INDEX = "foo";
|
||||
public static final String ROLE_A = "roleA";
|
||||
|
@ -35,7 +36,7 @@ public class CustomInMemoryRolesProvider
|
|||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> roles, ActionListener<Set<RoleDescriptor>> listener) {
|
||||
public void accept(Set<String> roles, ActionListener<RoleRetrievalResult> listener) {
|
||||
Set<RoleDescriptor> roleDescriptors = new HashSet<>();
|
||||
for (String role : roles) {
|
||||
if (rolePermissionSettings.containsKey(role)) {
|
||||
|
@ -52,6 +53,6 @@ public class CustomInMemoryRolesProvider
|
|||
}
|
||||
}
|
||||
|
||||
listener.onResponse(roleDescriptors);
|
||||
listener.onResponse(RoleRetrievalResult.success(roleDescriptors));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue