Clear Realm Caches on role mapping health change (elastic/x-pack-elasticsearch#3782)
If any of the follow take place on security index, then any cached role mappings are potentially invalid and the associated realms need to clear any cached users. - Index recovers from red - Index is deleted - Index becomes out-of-date / not-out-of-date Original commit: elastic/x-pack-elasticsearch@1bcd86fcd4
This commit is contained in:
parent
a627fec53e
commit
415bb7f039
|
@ -424,6 +424,9 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
|
|||
components.add(realms);
|
||||
components.add(reservedRealm);
|
||||
|
||||
securityLifecycleService.addSecurityIndexHealthChangeListener(nativeRoleMappingStore::onSecurityIndexHealthChange);
|
||||
securityLifecycleService.addSecurityIndexOutOfDateListener(nativeRoleMappingStore::onSecurityIndexOutOfDateChange);
|
||||
|
||||
AuthenticationFailureHandler failureHandler = null;
|
||||
String extensionName = null;
|
||||
for (SecurityExtension extension : securityExtensions) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.elasticsearch.client.Client;
|
|||
import org.elasticsearch.cluster.ClusterChangedEvent;
|
||||
import org.elasticsearch.cluster.ClusterState;
|
||||
import org.elasticsearch.cluster.ClusterStateListener;
|
||||
import org.elasticsearch.cluster.health.ClusterHealthStatus;
|
||||
import org.elasticsearch.cluster.health.ClusterIndexHealth;
|
||||
import org.elasticsearch.cluster.service.ClusterService;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
|
@ -214,4 +215,21 @@ public class SecurityLifecycleService extends AbstractComponent implements Clust
|
|||
public boolean isSecurityIndexOutOfDate() {
|
||||
return securityIndex.isIndexUpToDate() == false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the move from {@code previousHealth} to {@code currentHealth} a move from an unhealthy ("RED") index state to a healthy
|
||||
* ("non-RED") state.
|
||||
*/
|
||||
public static boolean isMoveFromRedToNonRed(ClusterIndexHealth previousHealth, ClusterIndexHealth currentHealth) {
|
||||
return (previousHealth == null || previousHealth.getStatus() == ClusterHealthStatus.RED)
|
||||
&& currentHealth != null && currentHealth.getStatus() != ClusterHealthStatus.RED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the move from {@code previousHealth} to {@code currentHealth} a move from index-exists to index-deleted
|
||||
*/
|
||||
public static boolean isIndexDeleted(ClusterIndexHealth previousHealth, ClusterIndexHealth currentHealth) {
|
||||
return previousHealth != null && currentHealth == null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.elasticsearch.action.index.IndexResponse;
|
|||
import org.elasticsearch.action.search.SearchRequest;
|
||||
import org.elasticsearch.action.support.ContextPreservingActionListener;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.cluster.health.ClusterIndexHealth;
|
||||
import org.elasticsearch.common.CheckedBiConsumer;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
|
@ -58,6 +59,8 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
|
|||
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
|
||||
import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin;
|
||||
import static org.elasticsearch.xpack.security.SecurityLifecycleService.SECURITY_INDEX_NAME;
|
||||
import static org.elasticsearch.xpack.security.SecurityLifecycleService.isIndexDeleted;
|
||||
import static org.elasticsearch.xpack.security.SecurityLifecycleService.isMoveFromRedToNonRed;
|
||||
|
||||
/**
|
||||
* This store reads + writes {@link ExpressionRoleMapping role mappings} in an Elasticsearch
|
||||
|
@ -79,6 +82,18 @@ public class NativeRoleMappingStore extends AbstractComponent implements UserRol
|
|||
|
||||
private static final String SECURITY_GENERIC_TYPE = "doc";
|
||||
|
||||
private static final ActionListener<Object> NO_OP_ACTION_LISTENER = new ActionListener<Object>() {
|
||||
@Override
|
||||
public void onResponse(Object o) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
// nothing
|
||||
}
|
||||
};
|
||||
|
||||
private final Client client;
|
||||
private final SecurityLifecycleService securityLifecycleService;
|
||||
private final List<String> realmsToRefresh = new CopyOnWriteArrayList<>();
|
||||
|
@ -301,6 +316,17 @@ public class NativeRoleMappingStore extends AbstractComponent implements UserRol
|
|||
listener.onResponse(usageStats);
|
||||
}
|
||||
|
||||
public void onSecurityIndexHealthChange(ClusterIndexHealth previousHealth, ClusterIndexHealth currentHealth) {
|
||||
if (isMoveFromRedToNonRed(previousHealth, currentHealth) || isIndexDeleted(previousHealth, currentHealth)) {
|
||||
refreshRealms(NO_OP_ACTION_LISTENER, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSecurityIndexOutOfDateChange(boolean prevOutOfDate, boolean outOfDate) {
|
||||
assert prevOutOfDate != outOfDate : "this method should only be called if the two values are different";
|
||||
refreshRealms(NO_OP_ACTION_LISTENER, null);
|
||||
}
|
||||
|
||||
private <Result> void refreshRealms(ActionListener<Result> listener, Result result) {
|
||||
String[] realmNames = this.realmsToRefresh.toArray(new String[realmsToRefresh.size()]);
|
||||
final SecurityClient securityClient = new SecurityClient(client);
|
||||
|
|
|
@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.authz.store;
|
|||
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.cluster.health.ClusterHealthStatus;
|
||||
import org.elasticsearch.cluster.health.ClusterIndexHealth;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
|
@ -34,6 +33,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
|
|||
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.security.SecurityLifecycleService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -53,6 +53,8 @@ import java.util.function.BiConsumer;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.elasticsearch.xpack.core.security.SecurityField.setting;
|
||||
import static org.elasticsearch.xpack.security.SecurityLifecycleService.isIndexDeleted;
|
||||
import static org.elasticsearch.xpack.security.SecurityLifecycleService.isMoveFromRedToNonRed;
|
||||
|
||||
/**
|
||||
* A composite roles store that combines built in roles, file-based roles, and index-based roles. Checks the built in roles first, then the
|
||||
|
@ -322,11 +324,7 @@ public class CompositeRolesStore extends AbstractComponent {
|
|||
}
|
||||
|
||||
public void onSecurityIndexHealthChange(ClusterIndexHealth previousHealth, ClusterIndexHealth currentHealth) {
|
||||
final boolean movedFromRedToNonRed = (previousHealth == null || previousHealth.getStatus() == ClusterHealthStatus.RED)
|
||||
&& currentHealth != null && currentHealth.getStatus() != ClusterHealthStatus.RED;
|
||||
final boolean indexDeleted = previousHealth != null && currentHealth == null;
|
||||
|
||||
if (movedFromRedToNonRed || indexDeleted) {
|
||||
if (isMoveFromRedToNonRed(previousHealth, currentHealth) || isIndexDeleted(previousHealth, currentHealth)) {
|
||||
invalidateAll();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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.security.authc.support.mapper;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.cluster.ClusterName;
|
||||
import org.elasticsearch.cluster.health.ClusterHealthStatus;
|
||||
import org.elasticsearch.cluster.health.ClusterIndexHealth;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
|
||||
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequest;
|
||||
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheResponse;
|
||||
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
import org.elasticsearch.xpack.core.security.user.User;
|
||||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.elasticsearch.xpack.security.test.SecurityTestUtils.getClusterIndexHealth;
|
||||
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.when;
|
||||
|
||||
public class NativeRoleMappingStoreTests extends ESTestCase {
|
||||
|
||||
public void testResolveRoles() throws Exception {
|
||||
// Does match DN
|
||||
final ExpressionRoleMapping mapping1 = new ExpressionRoleMapping("dept_h",
|
||||
new FieldExpression("dn", Collections.singletonList(new FieldValue("*,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
Arrays.asList("dept_h", "defence"), Collections.emptyMap(), true);
|
||||
// Does not match - user is not in this group
|
||||
final ExpressionRoleMapping mapping2 = new ExpressionRoleMapping("admin",
|
||||
new FieldExpression("groups", Collections.singletonList(
|
||||
new FieldValue(randomiseDn("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))),
|
||||
Arrays.asList("admin"), Collections.emptyMap(), true);
|
||||
// Does match - user is one of these groups
|
||||
final ExpressionRoleMapping mapping3 = new ExpressionRoleMapping("flight",
|
||||
new FieldExpression("groups", Arrays.asList(
|
||||
new FieldValue(randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")),
|
||||
new FieldValue(randomiseDn("cn=betaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")),
|
||||
new FieldValue(randomiseDn("cn=gammaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))
|
||||
)),
|
||||
Arrays.asList("flight"), Collections.emptyMap(), true);
|
||||
// Does not match - mapping is not enabled
|
||||
final ExpressionRoleMapping mapping4 = new ExpressionRoleMapping("mutants",
|
||||
new FieldExpression("groups", Collections.singletonList(
|
||||
new FieldValue(randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))),
|
||||
Arrays.asList("mutants"), Collections.emptyMap(), false);
|
||||
|
||||
final Client client = mock(Client.class);
|
||||
final SecurityLifecycleService lifecycleService = mock(SecurityLifecycleService.class);
|
||||
when(lifecycleService.isSecurityIndexAvailable()).thenReturn(true);
|
||||
|
||||
final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, lifecycleService) {
|
||||
@Override
|
||||
protected void loadMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
|
||||
final List<ExpressionRoleMapping> mappings = Arrays.asList(mapping1, mapping2, mapping3, mapping4);
|
||||
logger.info("Role mappings are: [{}]", mappings);
|
||||
listener.onResponse(mappings);
|
||||
}
|
||||
};
|
||||
|
||||
final RealmConfig realm = new RealmConfig("ldap1", Settings.EMPTY, Settings.EMPTY, mock(Environment.class),
|
||||
new ThreadContext(Settings.EMPTY));
|
||||
|
||||
final PlainActionFuture<Set<String>> future = new PlainActionFuture<>();
|
||||
final UserRoleMapper.UserData user = new UserRoleMapper.UserData("sasquatch",
|
||||
randomiseDn("cn=walter.langowski,ou=people,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
Arrays.asList(
|
||||
randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")
|
||||
), Collections.emptyMap(), realm);
|
||||
|
||||
logger.info("UserData is [{}]", user);
|
||||
store.resolveRoles(user, future);
|
||||
final Set<String> roles = future.get();
|
||||
assertThat(roles, Matchers.containsInAnyOrder("dept_h", "defence", "flight"));
|
||||
}
|
||||
|
||||
private String randomiseDn(String dn) {
|
||||
// Randomly transform the dn into another valid form that is logically identical,
|
||||
// but (potentially) textually different
|
||||
switch (randomIntBetween(0, 3)) {
|
||||
case 0:
|
||||
// do nothing
|
||||
return dn;
|
||||
case 1:
|
||||
return dn.toUpperCase(Locale.ROOT);
|
||||
case 2:
|
||||
// Upper case just the attribute name for each RDN
|
||||
return Arrays.stream(dn.split(",")).map(s -> {
|
||||
final String[] arr = s.split("=");
|
||||
arr[0] = arr[0].toUpperCase(Locale.ROOT);
|
||||
return String.join("=", arr);
|
||||
}).collect(Collectors.joining(","));
|
||||
case 3:
|
||||
return dn.replaceAll(",", ", ");
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
|
||||
|
||||
public void testCacheClearOnIndexHealthChange() {
|
||||
final AtomicInteger numInvalidation = new AtomicInteger(0);
|
||||
final NativeRoleMappingStore store = buildRoleMappingStoreForInvalidationTesting(numInvalidation);
|
||||
|
||||
int expectedInvalidation = 0;
|
||||
// existing to no longer present
|
||||
ClusterIndexHealth previousHealth = getClusterIndexHealth(randomFrom(ClusterHealthStatus.GREEN, ClusterHealthStatus.YELLOW));
|
||||
ClusterIndexHealth currentHealth = null;
|
||||
store.onSecurityIndexHealthChange(previousHealth, currentHealth);
|
||||
assertEquals(++expectedInvalidation, numInvalidation.get());
|
||||
|
||||
// doesn't exist to exists
|
||||
previousHealth = null;
|
||||
currentHealth = getClusterIndexHealth(randomFrom(ClusterHealthStatus.GREEN, ClusterHealthStatus.YELLOW));
|
||||
store.onSecurityIndexHealthChange(previousHealth, currentHealth);
|
||||
assertEquals(++expectedInvalidation, numInvalidation.get());
|
||||
|
||||
// green or yellow to red
|
||||
previousHealth = getClusterIndexHealth(randomFrom(ClusterHealthStatus.GREEN, ClusterHealthStatus.YELLOW));
|
||||
currentHealth = getClusterIndexHealth(ClusterHealthStatus.RED);
|
||||
store.onSecurityIndexHealthChange(previousHealth, currentHealth);
|
||||
assertEquals(expectedInvalidation, numInvalidation.get());
|
||||
|
||||
// red to non red
|
||||
previousHealth = getClusterIndexHealth(ClusterHealthStatus.RED);
|
||||
currentHealth = getClusterIndexHealth(randomFrom(ClusterHealthStatus.GREEN, ClusterHealthStatus.YELLOW));
|
||||
store.onSecurityIndexHealthChange(previousHealth, currentHealth);
|
||||
assertEquals(++expectedInvalidation, numInvalidation.get());
|
||||
|
||||
// green to yellow or yellow to green
|
||||
previousHealth = getClusterIndexHealth(randomFrom(ClusterHealthStatus.GREEN, ClusterHealthStatus.YELLOW));
|
||||
currentHealth = getClusterIndexHealth(
|
||||
previousHealth.getStatus() == ClusterHealthStatus.GREEN ? ClusterHealthStatus.YELLOW : ClusterHealthStatus.GREEN);
|
||||
store.onSecurityIndexHealthChange(previousHealth, currentHealth);
|
||||
assertEquals(expectedInvalidation, numInvalidation.get());
|
||||
}
|
||||
|
||||
public void testCacheClearOnIndexOutOfDateChange() {
|
||||
final AtomicInteger numInvalidation = new AtomicInteger(0);
|
||||
final NativeRoleMappingStore store = buildRoleMappingStoreForInvalidationTesting(numInvalidation);
|
||||
|
||||
store.onSecurityIndexOutOfDateChange(false, true);
|
||||
assertEquals(1, numInvalidation.get());
|
||||
|
||||
store.onSecurityIndexOutOfDateChange(true, false);
|
||||
assertEquals(2, numInvalidation.get());
|
||||
}
|
||||
|
||||
private NativeRoleMappingStore buildRoleMappingStoreForInvalidationTesting(AtomicInteger invalidationCounter) {
|
||||
final Settings settings = Settings.builder().put("path.home", createTempDir()).build();
|
||||
|
||||
final ThreadPool threadPool = mock(ThreadPool.class);
|
||||
final ThreadContext threadContext = new ThreadContext(settings);
|
||||
when(threadPool.getThreadContext()).thenReturn(threadContext);
|
||||
|
||||
final Client client = mock(Client.class);
|
||||
when(client.threadPool()).thenReturn(threadPool);
|
||||
when(client.settings()).thenReturn(settings);
|
||||
doAnswer(invocationOnMock -> {
|
||||
ActionListener<ClearRealmCacheResponse> listener = (ActionListener<ClearRealmCacheResponse>) invocationOnMock.getArguments()[2];
|
||||
invalidationCounter.incrementAndGet();
|
||||
listener.onResponse(new ClearRealmCacheResponse(new ClusterName("cluster"), Collections.emptyList(), Collections.emptyList()));
|
||||
return null;
|
||||
}).when(client).execute(eq(ClearRealmCacheAction.INSTANCE), any(ClearRealmCacheRequest.class), any(ActionListener.class));
|
||||
|
||||
final Environment env = TestEnvironment.newEnvironment(settings);
|
||||
final RealmConfig realmConfig = new RealmConfig(getTestName(), Settings.EMPTY, settings, env, threadContext);
|
||||
final CachingUsernamePasswordRealm mockRealm = new CachingUsernamePasswordRealm("test", realmConfig) {
|
||||
@Override
|
||||
protected void doAuthenticate(UsernamePasswordToken token, ActionListener<AuthenticationResult> listener) {
|
||||
listener.onResponse(AuthenticationResult.notHandled());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLookupUser(String username, ActionListener<User> listener) {
|
||||
listener.onResponse(null);
|
||||
}
|
||||
};
|
||||
final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, mock(SecurityLifecycleService.class));
|
||||
store.refreshRealmOnChange(mockRealm);
|
||||
return store;
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* 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.security.authc.support.mapper;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
|
||||
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue;
|
||||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeUserRoleMapperTests extends ESTestCase {
|
||||
|
||||
public void testResolveRoles() throws Exception {
|
||||
// Does match DN
|
||||
final ExpressionRoleMapping mapping1 = new ExpressionRoleMapping("dept_h",
|
||||
new FieldExpression("dn", Collections.singletonList(new FieldValue("*,ou=dept_h,o=forces,dc=gc,dc=ca"))),
|
||||
Arrays.asList("dept_h", "defence"), Collections.emptyMap(), true);
|
||||
// Does not match - user is not in this group
|
||||
final ExpressionRoleMapping mapping2 = new ExpressionRoleMapping("admin",
|
||||
new FieldExpression("groups", Collections.singletonList(
|
||||
new FieldValue(randomiseDn("cn=esadmin,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))),
|
||||
Arrays.asList("admin"), Collections.emptyMap(), true);
|
||||
// Does match - user is one of these groups
|
||||
final ExpressionRoleMapping mapping3 = new ExpressionRoleMapping("flight",
|
||||
new FieldExpression("groups", Arrays.asList(
|
||||
new FieldValue(randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")),
|
||||
new FieldValue(randomiseDn("cn=betaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")),
|
||||
new FieldValue(randomiseDn("cn=gammaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"))
|
||||
)),
|
||||
Arrays.asList("flight"), Collections.emptyMap(), true);
|
||||
// Does not match - mapping is not enabled
|
||||
final ExpressionRoleMapping mapping4 = new ExpressionRoleMapping("mutants",
|
||||
new FieldExpression("groups", Collections.singletonList(
|
||||
new FieldValue(randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")))),
|
||||
Arrays.asList("mutants"), Collections.emptyMap(), false);
|
||||
|
||||
final Client client = mock(Client.class);
|
||||
final SecurityLifecycleService lifecycleService = mock(SecurityLifecycleService.class);
|
||||
when(lifecycleService.isSecurityIndexAvailable()).thenReturn(true);
|
||||
|
||||
final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, lifecycleService) {
|
||||
@Override
|
||||
protected void loadMappings(ActionListener<List<ExpressionRoleMapping>> listener) {
|
||||
final List<ExpressionRoleMapping> mappings = Arrays.asList(mapping1, mapping2, mapping3, mapping4);
|
||||
logger.info("Role mappings are: [{}]", mappings);
|
||||
listener.onResponse(mappings);
|
||||
}
|
||||
};
|
||||
|
||||
final RealmConfig realm = new RealmConfig("ldap1", Settings.EMPTY, Settings.EMPTY, mock(Environment.class),
|
||||
new ThreadContext(Settings.EMPTY));
|
||||
|
||||
final PlainActionFuture<Set<String>> future = new PlainActionFuture<>();
|
||||
final UserRoleMapper.UserData user = new UserRoleMapper.UserData("sasquatch",
|
||||
randomiseDn("cn=walter.langowski,ou=people,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
Arrays.asList(
|
||||
randomiseDn("cn=alphaflight,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca"),
|
||||
randomiseDn("cn=mutants,ou=groups,ou=dept_h,o=forces,dc=gc,dc=ca")
|
||||
), Collections.emptyMap(), realm);
|
||||
|
||||
logger.info("UserData is [{}]", user);
|
||||
store.resolveRoles(user, future);
|
||||
final Set<String> roles = future.get();
|
||||
assertThat(roles, Matchers.containsInAnyOrder("dept_h", "defence", "flight"));
|
||||
}
|
||||
|
||||
private String randomiseDn(String dn) {
|
||||
// Randomly transform the dn into another valid form that is logically identical,
|
||||
// but (potentially) textually different
|
||||
switch (randomIntBetween(0, 3)) {
|
||||
case 0:
|
||||
// do nothing
|
||||
return dn;
|
||||
case 1:
|
||||
return dn.toUpperCase(Locale.ROOT);
|
||||
case 2:
|
||||
// Upper case just the attribute name for each RDN
|
||||
return Arrays.stream(dn.split(",")).map(s -> {
|
||||
final String[] arr = s.split("=");
|
||||
arr[0] = arr[0].toUpperCase(Locale.ROOT);
|
||||
return String.join("=", arr);
|
||||
}).collect(Collectors.joining(","));
|
||||
case 3:
|
||||
return dn.replaceAll(",", ", ");
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue