From 415bb7f0395643d456bc950b0285afbcde1c7932 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 1 Feb 2018 10:01:57 +1100 Subject: [PATCH] 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@1bcd86fcd4ea8cbd1388c9e7367704faac316574 --- .../xpack/security/Security.java | 3 + .../security/SecurityLifecycleService.java | 18 ++ .../mapper/NativeRoleMappingStore.java | 26 +++ .../authz/store/CompositeRolesStore.java | 10 +- .../mapper/NativeRoleMappingStoreTests.java | 211 ++++++++++++++++++ .../mapper/NativeUserRoleMapperTests.java | 111 --------- 6 files changed, 262 insertions(+), 117 deletions(-) create mode 100644 plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java delete mode 100644 plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeUserRoleMapperTests.java diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 9d671d413cd..9fd1693b394 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -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) { diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityLifecycleService.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityLifecycleService.java index a9c12b2c7e5..ca335f8ae1d 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityLifecycleService.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityLifecycleService.java @@ -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; + } + } diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java index e83b9cdc747..771119af6f9 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStore.java @@ -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 NO_OP_ACTION_LISTENER = new ActionListener() { + @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 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 void refreshRealms(ActionListener listener, Result result) { String[] realmNames = this.realmsToRefresh.toArray(new String[realmsToRefresh.size()]); final SecurityClient securityClient = new SecurityClient(client); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index dfe3a51750c..de6547f6369 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -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(); } } diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java new file mode 100644 index 00000000000..41fe340d05f --- /dev/null +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java @@ -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> listener) { + final List 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> 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 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 listener = (ActionListener) 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 listener) { + listener.onResponse(AuthenticationResult.notHandled()); + } + + @Override + protected void doLookupUser(String username, ActionListener listener) { + listener.onResponse(null); + } + }; + final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, client, mock(SecurityLifecycleService.class)); + store.refreshRealmOnChange(mockRealm); + return store; + } +} diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeUserRoleMapperTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeUserRoleMapperTests.java deleted file mode 100644 index 4b253c7ee81..00000000000 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeUserRoleMapperTests.java +++ /dev/null @@ -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> listener) { - final List 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> 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 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; - } - -}