Enables X-Pack extensions to implement custom roles providers (elastic/x-pack-elasticsearch#603)
This commit adds the ability for x-pack extensions to optionally provide custom roles providers, which are used to resolve any roles into role descriptors that are not found in the reserved or native realms. This feature enables the ability to define and provide roles from other sources, without having to pre-define such roles in the security config files. relates elastic/x-pack-elasticsearch#77 Original commit: elastic/x-pack-elasticsearch@bbbe7a49bf
This commit is contained in:
parent
02579c7acc
commit
b20578b9f6
|
@ -6,11 +6,13 @@
|
|||
package org.elasticsearch.xpack.common;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.inject.internal.Nullable;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* This action listener wraps another listener and provides a framework for iteration over a List while calling an asynchronous function
|
||||
|
@ -19,7 +21,8 @@ import java.util.function.BiConsumer;
|
|||
* listener's {@link ActionListener#onFailure(Exception)} method is called. If the consumer calls {@link #onResponse(Object)} with a
|
||||
* non-null object, iteration will cease and the wrapped listener will be called with the response. In the case of a null value being passed
|
||||
* to {@link #onResponse(Object)} then iteration will continue by applying the {@link BiConsumer} to the next item in the list; if the list
|
||||
* has no more elements then the wrapped listener will be called with a null value.
|
||||
* has no more elements then the wrapped listener will be called with a null value, unless an optional {@link Supplier} is provided
|
||||
* that supplies the response to send for {@link ActionListener#onResponse(Object)}.
|
||||
*
|
||||
* After creation, iteration is started by calling {@link #run()}
|
||||
*/
|
||||
|
@ -29,15 +32,42 @@ 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 int position = 0;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public IteratingActionListener(ActionListener<T> delegate, BiConsumer<U, ActionListener<T>> consumer, List<U> consumables,
|
||||
ThreadContext threadContext) {
|
||||
this(delegate, consumer, consumables, threadContext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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).
|
||||
*/
|
||||
public IteratingActionListener(ActionListener<T> delegate, BiConsumer<U, ActionListener<T>> consumer, List<U> consumables,
|
||||
ThreadContext threadContext, @Nullable Supplier<T> consumablesFinishedResponse) {
|
||||
this.delegate = delegate;
|
||||
this.consumer = consumer;
|
||||
this.consumables = Collections.unmodifiableList(consumables);
|
||||
this.threadContext = threadContext;
|
||||
this.consumablesFinishedResponse = consumablesFinishedResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,7 +90,11 @@ public final class IteratingActionListener<T, U> implements ActionListener<T>, R
|
|||
try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(false)) {
|
||||
if (response == null) {
|
||||
if (position == consumables.size()) {
|
||||
delegate.onResponse(null);
|
||||
if (consumablesFinishedResponse != null) {
|
||||
delegate.onResponse(consumablesFinishedResponse.get());
|
||||
} else {
|
||||
delegate.onResponse(null);
|
||||
}
|
||||
} else {
|
||||
consumer.accept(consumables.get(position++), this);
|
||||
}
|
||||
|
|
|
@ -10,12 +10,16 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.elasticsearch.xpack.security.authc.AuthenticationFailureHandler;
|
||||
import org.elasticsearch.xpack.security.authc.Realm;
|
||||
import org.elasticsearch.xpack.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -82,4 +86,29 @@ public abstract class XPackExtension {
|
|||
public List<String> getSettingsFilter() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ordered list of role providers that are used to resolve role names
|
||||
* to {@link RoleDescriptor} objects. Each provider is invoked in order to
|
||||
* resolve any role names not resolved by the reserved or native roles stores.
|
||||
*
|
||||
* Each role provider is represented as a {@link BiConsumer} which takes a set
|
||||
* of roles to resolve as the first parameter to consume and an {@link ActionListener}
|
||||
* as the second parameter to consume. The implementation of the role provider
|
||||
* 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)}.
|
||||
*
|
||||
* By default, an empty list is returned.
|
||||
*
|
||||
* @param settings The configured settings for the node
|
||||
* @param resourceWatcherService Use to watch configuration files for changes
|
||||
*/
|
||||
public List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>>
|
||||
getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security;
|
|||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.lucene.util.SetOnce;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.action.support.ActionFilter;
|
||||
|
@ -100,6 +101,7 @@ import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory;
|
|||
import org.elasticsearch.xpack.security.authc.support.SecuredString;
|
||||
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.accesscontrol.OptOutQueryCache;
|
||||
import org.elasticsearch.xpack.security.authz.accesscontrol.SecurityIndexSearcherWrapper;
|
||||
import org.elasticsearch.xpack.security.authz.accesscontrol.SetSecurityUserProcessor;
|
||||
|
@ -143,6 +145,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
@ -329,8 +332,12 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
|
|||
final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, licenseState);
|
||||
final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, licenseState, securityLifecycleService);
|
||||
final ReservedRolesStore reservedRolesStore = new ReservedRolesStore();
|
||||
final CompositeRolesStore allRolesStore =
|
||||
new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, licenseState);
|
||||
List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> rolesProviders = new ArrayList<>();
|
||||
for (XPackExtension extension : extensions) {
|
||||
rolesProviders.addAll(extension.getRolesProviders(settings, resourceWatcherService));
|
||||
}
|
||||
final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore,
|
||||
reservedRolesStore, rolesProviders, threadPool.getThreadContext(), licenseState);
|
||||
// to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be
|
||||
// minimal
|
||||
licenseState.addListener(allRolesStore::invalidateAll);
|
||||
|
|
|
@ -15,10 +15,12 @@ import org.elasticsearch.common.inject.internal.Nullable;
|
|||
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.ThreadContext;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
|
||||
import org.elasticsearch.common.util.concurrent.ReleasableLock;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.xpack.common.IteratingActionListener;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
|
||||
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsCache;
|
||||
|
@ -32,12 +34,14 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
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.stream.Collectors;
|
||||
|
||||
import static org.elasticsearch.xpack.security.Security.setting;
|
||||
|
@ -70,10 +74,15 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
private final XPackLicenseState licenseState;
|
||||
private final Cache<Set<String>, Role> roleCache;
|
||||
private final Set<String> negativeLookupCache;
|
||||
private final ThreadContext threadContext;
|
||||
private final AtomicLong numInvalidation = new AtomicLong();
|
||||
private final List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> customRolesProviders;
|
||||
|
||||
public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore,
|
||||
ReservedRolesStore reservedRolesStore, XPackLicenseState licenseState) {
|
||||
ReservedRolesStore reservedRolesStore,
|
||||
List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>> rolesProviders,
|
||||
ThreadContext threadContext,
|
||||
XPackLicenseState licenseState) {
|
||||
super(settings);
|
||||
this.fileRolesStore = fileRolesStore;
|
||||
// invalidating all on a file based role update is heavy handed to say the least, but in general this should be infrequent so the
|
||||
|
@ -88,7 +97,9 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
builder.setMaximumWeight(cacheSize);
|
||||
}
|
||||
this.roleCache = builder.build();
|
||||
this.threadContext = threadContext;
|
||||
this.negativeLookupCache = ConcurrentCollections.newConcurrentSet();
|
||||
this.customRolesProviders = Collections.unmodifiableList(rolesProviders);
|
||||
}
|
||||
|
||||
public void roles(Set<String> roleNames, FieldPermissionsCache fieldPermissionsCache, ActionListener<Role> roleActionListener) {
|
||||
|
@ -141,9 +152,34 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
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";
|
||||
negativeLookupCache.addAll(missing);
|
||||
if (!customRolesProviders.isEmpty()) {
|
||||
new IteratingActionListener<>(roleDescriptorActionListener, (rolesProvider, listener) -> {
|
||||
// resolve descriptors with role provider
|
||||
rolesProvider.accept(missing, ActionListener.wrap((resolvedDescriptors) -> {
|
||||
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 {
|
||||
negativeLookupCache.addAll(missing);
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
}
|
||||
} else {
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
}
|
||||
roleDescriptorActionListener.onResponse(Collections.unmodifiableSet(builtInRoleDescriptors));
|
||||
}, roleDescriptorActionListener::onFailure));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authz.store;
|
|||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
|
@ -16,12 +17,20 @@ import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
|
|||
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsCache;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.elasticsearch.mock.orig.Mockito.times;
|
||||
import static org.elasticsearch.mock.orig.Mockito.verifyNoMoreInteractions;
|
||||
import static org.hamcrest.Matchers.anyOf;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anySetOf;
|
||||
import static org.mockito.Matchers.eq;
|
||||
|
@ -73,7 +82,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
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), licenseState);
|
||||
mock(ReservedRolesStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), licenseState);
|
||||
|
||||
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
PlainActionFuture<Role> roleFuture = new PlainActionFuture<>();
|
||||
|
@ -132,7 +141,7 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
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), licenseState);
|
||||
mock(ReservedRolesStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), licenseState);
|
||||
|
||||
FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
PlainActionFuture<Role> roleFuture = new PlainActionFuture<>();
|
||||
|
@ -164,7 +173,8 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, new XPackLicenseState());
|
||||
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore,
|
||||
Collections.emptyList(), new ThreadContext(Settings.EMPTY), new XPackLicenseState());
|
||||
verify(fileRolesStore).addListener(any(Runnable.class)); // adds a listener in ctor
|
||||
|
||||
final String roleName = randomAsciiOfLengthBetween(1, 10);
|
||||
|
@ -193,4 +203,132 @@ public class CompositeRolesStoreTests extends ESTestCase {
|
|||
}
|
||||
verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore);
|
||||
}
|
||||
|
||||
public void testCustomRolesProviders() {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore());
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider1 = spy(new InMemoryRolesProvider((roles) -> {
|
||||
Set<RoleDescriptor> descriptors = new HashSet<>();
|
||||
if (roles.contains("roleA")) {
|
||||
descriptors.add(new RoleDescriptor("roleA", null,
|
||||
new IndicesPrivileges[] {
|
||||
IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
}));
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider2 = spy(new InMemoryRolesProvider((roles) -> {
|
||||
Set<RoleDescriptor> descriptors = new HashSet<>();
|
||||
if (roles.contains("roleA")) {
|
||||
// both role providers can resolve role A, this makes sure that if the first
|
||||
// role provider in order resolves a role, the second provider does not override it
|
||||
descriptors.add(new RoleDescriptor("roleA", null,
|
||||
new IndicesPrivileges[] {
|
||||
IndicesPrivileges.builder().privileges("WRITE").indices("*").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
if (roles.contains("roleB")) {
|
||||
descriptors.add(new RoleDescriptor("roleB", null,
|
||||
new IndicesPrivileges[] {
|
||||
IndicesPrivileges.builder().privileges("READ").indices("bar").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
}));
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore,
|
||||
Arrays.asList(inMemoryProvider1, inMemoryProvider2), new ThreadContext(Settings.EMPTY),
|
||||
new XPackLicenseState());
|
||||
|
||||
final Set<String> roleNames = Sets.newHashSet("roleA", "roleB", "unknown");
|
||||
PlainActionFuture<Role> future = new PlainActionFuture<>();
|
||||
final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
compositeRolesStore.roles(roleNames, fieldPermissionsCache, future);
|
||||
final Role role = future.actionGet();
|
||||
|
||||
// make sure custom roles providers populate roles correctly
|
||||
assertEquals(2, role.indices().groups().length);
|
||||
assertEquals(IndexPrivilege.READ, role.indices().groups()[0].privilege());
|
||||
assertThat(role.indices().groups()[0].indices()[0], anyOf(equalTo("foo"), equalTo("bar")));
|
||||
assertEquals(IndexPrivilege.READ, role.indices().groups()[1].privilege());
|
||||
assertThat(role.indices().groups()[1].indices()[0], anyOf(equalTo("foo"), equalTo("bar")));
|
||||
|
||||
// make sure negative lookups are cached
|
||||
verify(inMemoryProvider1).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
verify(inMemoryProvider2).accept(anySetOf(String.class), any(ActionListener.class));
|
||||
|
||||
final int numberOfTimesToCall = scaledRandomIntBetween(1, 8);
|
||||
for (int i = 0; i < numberOfTimesToCall; i++) {
|
||||
future = new PlainActionFuture<>();
|
||||
compositeRolesStore.roles(Collections.singleton("unknown"), fieldPermissionsCache, future);
|
||||
future.actionGet();
|
||||
}
|
||||
|
||||
verifyNoMoreInteractions(inMemoryProvider1, inMemoryProvider2);
|
||||
}
|
||||
|
||||
public void testCustomRolesProviderFailures() throws Exception {
|
||||
final FileRolesStore fileRolesStore = mock(FileRolesStore.class);
|
||||
when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet());
|
||||
final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class);
|
||||
doAnswer((invocationOnMock) -> {
|
||||
ActionListener<Set<RoleDescriptor>> callback = (ActionListener<Set<RoleDescriptor>>) invocationOnMock.getArguments()[1];
|
||||
callback.onResponse(Collections.emptySet());
|
||||
return null;
|
||||
}).when(nativeRolesStore).getRoleDescriptors(isA(String[].class), any(ActionListener.class));
|
||||
final ReservedRolesStore reservedRolesStore = new ReservedRolesStore();
|
||||
|
||||
final InMemoryRolesProvider inMemoryProvider1 = new InMemoryRolesProvider((roles) -> {
|
||||
Set<RoleDescriptor> descriptors = new HashSet<>();
|
||||
if (roles.contains("roleA")) {
|
||||
descriptors.add(new RoleDescriptor("roleA", null,
|
||||
new IndicesPrivileges[] {
|
||||
IndicesPrivileges.builder().privileges("READ").indices("foo").grantedFields("*").build()
|
||||
}, null));
|
||||
}
|
||||
return descriptors;
|
||||
});
|
||||
|
||||
final BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> failingProvider =
|
||||
(roles, listener) -> listener.onFailure(new Exception("fake failure"));
|
||||
|
||||
final CompositeRolesStore compositeRolesStore =
|
||||
new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore,
|
||||
Arrays.asList(inMemoryProvider1, failingProvider), new ThreadContext(Settings.EMPTY),
|
||||
new XPackLicenseState());
|
||||
|
||||
final Set<String> roleNames = Sets.newHashSet("roleA", "roleB", "unknown");
|
||||
PlainActionFuture<Role> future = new PlainActionFuture<>();
|
||||
final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
|
||||
compositeRolesStore.roles(roleNames, fieldPermissionsCache, future);
|
||||
try {
|
||||
future.get();
|
||||
fail("provider should have thrown a failure");
|
||||
} catch (ExecutionException e) {
|
||||
assertEquals("fake failure", e.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryRolesProvider implements BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> {
|
||||
private final Function<Set<String>, Set<RoleDescriptor>> roleDescriptorsFunc;
|
||||
|
||||
InMemoryRolesProvider(Function<Set<String>, Set<RoleDescriptor>> roleDescriptorsFunc) {
|
||||
this.roleDescriptorsFunc = roleDescriptorsFunc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> roles, ActionListener<Set<RoleDescriptor>> listener) {
|
||||
listener.onResponse(roleDescriptorsFunc.apply(roles));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ integTestCluster {
|
|||
setting 'xpack.security.authc.realms.custom.filtered_setting', 'should be filtered'
|
||||
setting 'xpack.security.authc.realms.esusers.order', '1'
|
||||
setting 'xpack.security.authc.realms.esusers.type', 'file'
|
||||
setting 'xpack.security.authc.realms.native.type', 'native'
|
||||
setting 'xpack.security.authc.realms.native.order', '2'
|
||||
|
||||
setupCommand 'setupDummyUser',
|
||||
'bin/x-pack/users', 'useradd', 'test_user', '-p', 'changeme', '-r', 'superuser'
|
|
@ -5,22 +5,35 @@
|
|||
*/
|
||||
package org.elasticsearch.example;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.example.realm.CustomAuthenticationFailureHandler;
|
||||
import org.elasticsearch.example.realm.CustomRealm;
|
||||
import org.elasticsearch.example.role.CustomInMemoryRolesProvider;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.elasticsearch.xpack.security.authc.AuthenticationFailureHandler;
|
||||
import org.elasticsearch.xpack.extensions.XPackExtension;
|
||||
import org.elasticsearch.xpack.security.authc.Realm;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class ExampleRealmExtension extends XPackExtension {
|
||||
import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.ROLE_A;
|
||||
import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.ROLE_B;
|
||||
|
||||
/**
|
||||
* An example x-pack extension for testing custom realms and custom role providers.
|
||||
*/
|
||||
public class ExampleExtension extends XPackExtension {
|
||||
|
||||
static {
|
||||
// check that the extension's policy works.
|
||||
|
@ -59,4 +72,15 @@ public class ExampleRealmExtension extends XPackExtension {
|
|||
public List<String> getSettingsFilter() {
|
||||
return Collections.singletonList("xpack.security.authc.realms.*.filtered_setting");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>>>
|
||||
getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) {
|
||||
CustomInMemoryRolesProvider rp1 = new CustomInMemoryRolesProvider(settings, Collections.singletonMap(ROLE_A, "read"));
|
||||
Map<String, String> roles = new HashMap<>();
|
||||
roles.put(ROLE_A, "all");
|
||||
roles.put(ROLE_B, "all");
|
||||
CustomInMemoryRolesProvider rp2 = new CustomInMemoryRolesProvider(settings, roles);
|
||||
return Arrays.asList(rp1, rp2);
|
||||
}
|
||||
}
|
|
@ -21,8 +21,8 @@ public class CustomRealm extends Realm {
|
|||
public static final String USER_HEADER = "User";
|
||||
public static final String PW_HEADER = "Password";
|
||||
|
||||
static final String KNOWN_USER = "custom_user";
|
||||
static final String KNOWN_PW = "changeme";
|
||||
public static final String KNOWN_USER = "custom_user";
|
||||
public static final String KNOWN_PW = "changeme";
|
||||
static final String[] ROLES = new String[] { "superuser" };
|
||||
|
||||
public CustomRealm(RealmConfig config) {
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.example.role;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* A custom roles provider implementation for testing that serves
|
||||
* static roles from memory.
|
||||
*/
|
||||
public class CustomInMemoryRolesProvider
|
||||
extends AbstractComponent
|
||||
implements BiConsumer<Set<String>, ActionListener<Set<RoleDescriptor>>> {
|
||||
|
||||
public static final String INDEX = "foo";
|
||||
public static final String ROLE_A = "roleA";
|
||||
public static final String ROLE_B = "roleB";
|
||||
|
||||
private final Map<String, String> rolePermissionSettings;
|
||||
|
||||
public CustomInMemoryRolesProvider(Settings settings, Map<String, String> rolePermissionSettings) {
|
||||
super(settings);
|
||||
this.rolePermissionSettings = rolePermissionSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Set<String> roles, ActionListener<Set<RoleDescriptor>> listener) {
|
||||
Set<RoleDescriptor> roleDescriptors = new HashSet<>();
|
||||
for (String role : roles) {
|
||||
if (rolePermissionSettings.containsKey(role)) {
|
||||
roleDescriptors.add(
|
||||
new RoleDescriptor(role, new String[] { "all" },
|
||||
new RoleDescriptor.IndicesPrivileges[] {
|
||||
RoleDescriptor.IndicesPrivileges.builder()
|
||||
.privileges(rolePermissionSettings.get(role))
|
||||
.indices(INDEX)
|
||||
.grantedFields("*")
|
||||
.build()
|
||||
}, null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
listener.onResponse(roleDescriptors);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
description=Custom Extension
|
||||
version=${version}
|
||||
name=exampleextension
|
||||
classname=org.elasticsearch.example.ExampleExtension
|
||||
java.version=${java.version}
|
||||
xpack.version=${xpack.version}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.example.role;
|
||||
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.client.ResponseException;
|
||||
import org.elasticsearch.common.network.NetworkModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.example.realm.CustomRealm;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.test.ESIntegTestCase;
|
||||
import org.elasticsearch.xpack.XPackPlugin;
|
||||
import org.elasticsearch.xpack.security.authc.support.SecuredString;
|
||||
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.xpack.security.client.SecurityClient;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.INDEX;
|
||||
import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.ROLE_A;
|
||||
import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.ROLE_B;
|
||||
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
* Integration test for custom roles providers.
|
||||
*/
|
||||
public class CustomRolesProviderIT extends ESIntegTestCase {
|
||||
|
||||
private static final String TEST_USER = "test_user";
|
||||
private static final String TEST_PWD = "change_me";
|
||||
|
||||
@Override
|
||||
protected Settings externalClusterClientSettings() {
|
||||
return Settings.builder()
|
||||
.put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
||||
.put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||
.put(NetworkModule.TRANSPORT_TYPE_KEY, "security4")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
|
||||
return Collections.singleton(XPackPlugin.class);
|
||||
}
|
||||
|
||||
public void setupTestUser(String role) {
|
||||
SecurityClient securityClient = new SecurityClient(client());
|
||||
securityClient.preparePutUser(TEST_USER, TEST_PWD.toCharArray(), role).get();
|
||||
}
|
||||
|
||||
public void testAuthorizedCustomRoleSucceeds() throws Exception {
|
||||
setupTestUser(ROLE_B);
|
||||
// roleB has all permissions on index "foo", so creating "foo" should succeed
|
||||
Response response = getRestClient().performRequest("PUT", "/" + INDEX, authHeader());
|
||||
assertThat(response.getStatusLine().getStatusCode(), is(200));
|
||||
}
|
||||
|
||||
public void testFirstResolvedRoleTakesPrecedence() throws Exception {
|
||||
// the first custom roles provider has set ROLE_A to only have read permission on the index,
|
||||
// the second custom roles provider has set ROLE_A to have all permissions, but since
|
||||
// the first custom role provider appears first in order, it should take precedence and deny
|
||||
// permission to create the index
|
||||
setupTestUser(ROLE_A);
|
||||
// roleB has all permissions on index "foo", so creating "foo" should succeed
|
||||
try {
|
||||
getRestClient().performRequest("PUT", "/" + INDEX, authHeader());
|
||||
fail(ROLE_A + " should not be authorized to create index " + INDEX);
|
||||
} catch (ResponseException e) {
|
||||
assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403));
|
||||
}
|
||||
}
|
||||
|
||||
public void testUnresolvedRoleDoesntSucceed() throws Exception {
|
||||
setupTestUser("unknown");
|
||||
// roleB has all permissions on index "foo", so creating "foo" should succeed
|
||||
try {
|
||||
getRestClient().performRequest("PUT", "/" + INDEX, authHeader());
|
||||
fail(ROLE_A + " should not be authorized to create index " + INDEX);
|
||||
} catch (ResponseException e) {
|
||||
assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403));
|
||||
}
|
||||
}
|
||||
|
||||
private BasicHeader authHeader() {
|
||||
return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER,
|
||||
basicAuthHeaderValue(TEST_USER, new SecuredString(TEST_PWD.toCharArray())));
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
description=Custom Realm Extension
|
||||
version=${version}
|
||||
name=examplerealm
|
||||
classname=org.elasticsearch.example.ExampleRealmExtension
|
||||
java.version=${java.version}
|
||||
xpack.version=${xpack.version}
|
Loading…
Reference in New Issue