Test: Functional tests for LDAP and Group Mappings

This adds a framework class for setting up random LDAP realms.  Two implementations test group mapping.

Fixes https://github.com/elasticsearch/elasticsearch-shield-qa/issues/15

Original commit: elastic/x-pack-elasticsearch@2bdc25e306
This commit is contained in:
c-a-m 2014-12-29 17:10:41 -07:00
parent 112b6a0e57
commit 00e17aabec
6 changed files with 385 additions and 18 deletions

View File

@ -0,0 +1,231 @@
/*
* 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.integration;
import com.carrotsearch.randomizedtesting.LifecycleScope;
import org.apache.lucene.util.AbstractRandomizedTest;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.authz.AuthorizationException;
import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.junit.BeforeClass;
import org.junit.Ignore;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.BASIC_AUTH_HEADER;
import static org.elasticsearch.shield.test.ShieldTestUtils.writeFile;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
/**
* This test assumes all subclass tests will be of type SUITE. It picks a random realm configuration for the tests, and
* writes a group to role mapping file for each node.
*/
@Ignore
@AbstractRandomizedTest.Integration
abstract public class AbstractAdLdapRealmTests extends ShieldIntegrationTest {
public static final String SHIELD_AUTHC_REALMS_EXTERNAL = "shield.authc.realms.external";
public static final String PASSWORD = "NickFuryHeartsES";
public static final String ASGARDIAN_INDEX = "gods";
public static final String PHILANTHROPISTS_INDEX = "philanthropists";
public static final String SHIELD_INDEX = "shield";
private static final String AD_ROLE_MAPPING =
"SHIELD: [ \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Avengers: [ \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Gods: [ \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n" +
"Philanthropists: [ \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\" ] \n";
private static final String OLDAP_ROLE_MAPPING =
"SHIELD: [ \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Avengers: [ \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Gods: [ \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n" +
"Philanthropists: [ \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\" ] \n";
static protected RealmConfig realmConfig;
@BeforeClass
public static void setupRealm() {
realmConfig = randomFrom(RealmConfig.values());
ESLoggerFactory.getLogger("test").info("Running test with realm configuration [{}], with direct group to role mapping [{}]",
realmConfig, realmConfig.mapGroupsAsRoles);
}
@Override
protected Settings nodeSettings(int nodeOrdinal) {
File nodeFiles = newTempDir(LifecycleScope.SUITE);
return ImmutableSettings.builder()
.put(super.nodeSettings(nodeOrdinal))
.put(realmConfig.buildSettings())
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".files.role_mapping", writeFile(nodeFiles, "role_mapping.yml", configRoleMappings()))
.build();
}
protected String configRoleMappings() {
return realmConfig.configRoleMappings();
}
@Override
protected String configRoles() {
return super.configRoles() +
"\n" +
"Avengers:\n" +
" cluster: NONE\n" +
" indices:\n" +
" 'avengers': ALL\n" +
"SHIELD:\n" +
" cluster: NONE\n" +
" indices:\n " +
" '" + SHIELD_INDEX + "': ALL\n" +
"Gods:\n" +
" cluster: NONE\n" +
" indices:\n" +
" '" + ASGARDIAN_INDEX + "': ALL\n" +
"Philanthropists:\n" +
" cluster: NONE\n" +
" indices:\n" +
" '" + PHILANTHROPISTS_INDEX + "': ALL\n";
}
protected void assertAccessAllowed(String user, String index) throws IOException {
IndexResponse indexResponse = client().prepareIndex(index, "type").
setSource(jsonBuilder()
.startObject()
.field("name", "value")
.endObject())
.putHeader(BASIC_AUTH_HEADER, userHeader(user, PASSWORD))
.execute().actionGet();
assertThat("user " + user + " should have write access to index " + index, indexResponse.isCreated(), is(true));
refresh();
GetResponse getResponse = client().prepareGet(index, "type", indexResponse.getId())
.putHeader(BASIC_AUTH_HEADER, userHeader(user, PASSWORD))
.get();
assertThat("user " + user + " should have read access to index " + index, getResponse.getId(), equalTo(indexResponse.getId()));
}
protected void assertAccessDenied(String user, String index) throws IOException {
try {
client().prepareIndex(index, "type").
setSource(jsonBuilder()
.startObject()
.field("name", "value")
.endObject())
.putHeader(BASIC_AUTH_HEADER, userHeader(user, PASSWORD))
.execute().actionGet();
fail("Write access to index " + index + " should not be allowed for user " + user);
} catch (AuthorizationException e) {
}
refresh();
}
protected static String userHeader(String username, String password) {
return UsernamePasswordToken.basicAuthHeaderValue(username, new SecuredString(password.toCharArray()));
}
private static Settings sslSettingsForStore(String resourcePathToStore, String password) {
File store;
try {
store = new File(AbstractAdLdapRealmTests.class.getResource(resourcePathToStore).toURI());
} catch (URISyntaxException e) {
throw new ElasticsearchException("exception while reading the store", e);
}
if (!store.exists()) {
throw new ElasticsearchException("store path doesn't exist");
}
return settingsBuilder()
.put("shield.ssl.keystore.path", store.getPath())
.put("shield.ssl.keystore.password", password)
.put(NettySecuredTransport.HOSTNAME_VERIFICATION_SETTING, false)
.put("shield.ssl.truststore.path", store.getPath())
.put("shield.ssl.truststore.password", password).build();
}
/**
* Represents multiple possible configurations for active directory and ldap
*/
enum RealmConfig {
AD(false, AD_ROLE_MAPPING,
ImmutableSettings.builder()
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", ActiveDirectoryRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".domain_name", "ad.test.elasticsearch.com")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://ad.test.elasticsearch.com:636")
.build()),
AD_LDAP_GROUPS_FROM_SEARCH(true, AD_ROLE_MAPPING,
ImmutableSettings.builder()
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", LdapRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://ad.test.elasticsearch.com:636")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.group_search_dn", "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.putArray(SHIELD_AUTHC_REALMS_EXTERNAL + ".user_dn_templates", "cn={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.build()),
AD_LDAP_GROUPS_FROM_ATTRIBUTE(true, AD_ROLE_MAPPING,
ImmutableSettings.builder()
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", LdapRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://ad.test.elasticsearch.com:636")
.putArray(SHIELD_AUTHC_REALMS_EXTERNAL + ".user_dn_templates", "cn={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.build()),
OLDAP(false, OLDAP_ROLE_MAPPING,
ImmutableSettings.builder()
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", LdapRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://54.200.235.244:636")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.group_search_dn", "ou=people, dc=oldap, dc=test, dc=elasticsearch, dc=com")
.putArray(SHIELD_AUTHC_REALMS_EXTERNAL + ".user_dn_templates", "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com")
.build());
final boolean mapGroupsAsRoles;
final boolean loginWithCommonName;
private final String roleMappings;
private final Settings settings;
RealmConfig(boolean loginWithCommonName, String roleMappings, Settings settings) {
this.settings = settings;
this.loginWithCommonName = loginWithCommonName;
this.roleMappings = roleMappings;
this.mapGroupsAsRoles = randomBoolean();
}
public Settings buildSettings() {
ImmutableSettings.Builder builder = ImmutableSettings.builder()
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".order", 1)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".hostname_verification", false)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.subtree_search", randomBoolean())
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".unmapped_groups_as_roles", mapGroupsAsRoles)
.put(sslSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode.jks", "testnode")) //we need ssl to the LDAP server
.put(this.settings);
return builder.build();
}
//if mapGroupsAsRoles is turned on we don't write anything to the rolemapping file
public String configRoleMappings() {
return mapGroupsAsRoles ? "" : roleMappings;
}
}
}

View File

@ -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.integration;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.Test;
import java.io.IOException;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.SUITE;
/**
* This tests the group to role mappings from LDAP sources provided by the super class - available from super.realmConfig.
* The super class will provide appropriate group mappings via configGroupMappings()
*/
@Network
@ClusterScope(scope = SUITE)
public class GroupMappingTests extends AbstractAdLdapRealmTests {
@Test
public void testAuthcAuthz() throws IOException {
String avenger = realmConfig.loginWithCommonName ? "Natasha Romanoff" : "blackwidow";
assertAccessAllowed(avenger, "avengers");
}
@Test
public void testGroupMapping() throws IOException {
String asgardian = "odin";
String shieldPhilanthropist = realmConfig.loginWithCommonName ? "Bruce Banner" : "hulk";
String shield = realmConfig.loginWithCommonName ? "Phil Coulson" : "phil";
String shieldAsgardianPhilanthropist = "thor";
String noGroupUser = "jarvis";
assertAccessAllowed(asgardian, ASGARDIAN_INDEX);
assertAccessAllowed(shieldAsgardianPhilanthropist, ASGARDIAN_INDEX);
assertAccessDenied(shieldPhilanthropist, ASGARDIAN_INDEX);
assertAccessDenied(shield, ASGARDIAN_INDEX);
assertAccessDenied(noGroupUser, ASGARDIAN_INDEX);
assertAccessAllowed(shieldPhilanthropist, PHILANTHROPISTS_INDEX);
assertAccessAllowed(shieldAsgardianPhilanthropist, PHILANTHROPISTS_INDEX);
assertAccessDenied(asgardian, PHILANTHROPISTS_INDEX);
assertAccessDenied(shield, PHILANTHROPISTS_INDEX);
assertAccessDenied(noGroupUser, PHILANTHROPISTS_INDEX);
assertAccessAllowed(shield, SHIELD_INDEX);
assertAccessAllowed(shieldPhilanthropist, SHIELD_INDEX);
assertAccessAllowed(shieldAsgardianPhilanthropist, SHIELD_INDEX);
assertAccessDenied(asgardian, SHIELD_INDEX);
assertAccessDenied(noGroupUser, SHIELD_INDEX);
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.integration;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.Test;
import java.io.IOException;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.SUITE;
/**
* This tests the mapping of multiple groups to a role
*/
@Network
@ClusterScope(scope = SUITE)
public class MultiGroupMappingTests extends AbstractAdLdapRealmTests {
@Override
protected String configRoles() {
return super.configRoles() +
"\n" +
"MarvelCharacters:\n" +
" cluster: NONE\n" +
" indices:\n" +
" 'marvel_comics': ALL\n";
}
@Override
protected String configRoleMappings() {
return "MarvelCharacters: \n" +
" - \"CN=SHIELD,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Gods,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"CN=Philanthropists,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com\"\n" +
" - \"cn=SHIELD,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Gods,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"\n" +
" - \"cn=Philanthropists,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com\"";
}
@Test
public void testGroupMapping() throws IOException {
String asgardian = "odin";
String shieldPhilanthropist = realmConfig.loginWithCommonName ? "Bruce Banner" : "hulk";
String shield = realmConfig.loginWithCommonName ? "Phil Coulson" : "phil";
String shieldAsgardianPhilanthropist = "thor";
String noGroupUser = "jarvis";
assertAccessAllowed(asgardian, "marvel_comics");
assertAccessAllowed(shieldAsgardianPhilanthropist, "marvel_comics");
assertAccessAllowed(shieldPhilanthropist, "marvel_comics");
assertAccessAllowed(shield, "marvel_comics");
assertAccessDenied(noGroupUser, "marvel_comics");
}
}

View File

@ -135,4 +135,8 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
UsernamePasswordToken token2 = new UsernamePasswordToken("username", new SecuredString("password".toCharArray()));
assertThat(token1, equalTo(token2));
}
public static String basicAuthHeaderValue(String username, String passwd) {
return UsernamePasswordToken.basicAuthHeaderValue(username, new SecuredString(passwd.toCharArray()));
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.shield.test;
import com.google.common.base.Charsets;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.io.Streams;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
public class ShieldTestUtils {
public static String writeFile(File folder, String name, byte[] content) {
Path file = folder.toPath().resolve(name);
try {
Streams.copy(content, file.toFile());
} catch (IOException e) {
throw new ElasticsearchException("Error writing file in test", e);
}
return file.toFile().getAbsolutePath();
}
public static String writeFile(File folder, String name, String content) {
return writeFile(folder, name, content.getBytes(Charsets.UTF_8));
}
}

View File

@ -6,11 +6,9 @@
package org.elasticsearch.test;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.google.common.base.Charsets;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.client.support.Headers;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.os.OsUtils;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
@ -25,12 +23,11 @@ import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
import org.elasticsearch.test.discovery.ClusterDiscoveryConfiguration;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.elasticsearch.shield.test.ShieldTestUtils.writeFile;
/**
* {@link org.elasticsearch.test.SettingsSource} subclass that allows to set all needed settings for shield.
@ -97,6 +94,7 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
.put("shield.audit.enabled", RandomizedTest.randomBoolean())
.put(InternalSignatureService.FILE_SETTING, writeFile(folder, "system_key", systemKey))
.put("shield.authc.realms.esusers.type", ESUsersRealm.TYPE)
.put("shield.authc.realms.esusers.order", 0)
.put("shield.authc.realms.esusers.files.users", writeFile(folder, "users", configUsers()))
.put("shield.authc.realms.esusers.files.users_roles", writeFile(folder, "users_roles", configUsersRoles()))
.put("shield.authz.store.files.roles", writeFile(folder, "roles.yml", configRoles()))
@ -181,20 +179,6 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
return createdFolder;
}
private static String writeFile(File folder, String name, byte[] content) {
Path file = folder.toPath().resolve(name);
try {
Streams.copy(content, file.toFile());
} catch (IOException e) {
throw new ElasticsearchException("Error writing file in test", e);
}
return file.toFile().getAbsolutePath();
}
private static String writeFile(File folder, String name, String content) {
return writeFile(folder, name, content.getBytes(Charsets.UTF_8));
}
private static byte[] generateKey() {
try {
return InternalSignatureService.generateKey();