Switch SAML QA test to use a standalone cluster (elastic/x-pack-elasticsearch#3743)

Introduce a healthy dose of reality into the SAML integration tests.
Switches the test class from *Tests to *IT, and updates the build to run it during integTest

Original commit: elastic/x-pack-elasticsearch@edd2538e5f
This commit is contained in:
Tim Vernum 2018-01-30 23:03:04 +11:00 committed by GitHub
parent 36ff4185a7
commit 427da8de8b
2 changed files with 97 additions and 130 deletions

View File

@ -1,8 +1,9 @@
Project idpFixtureProject = xpackProject("test:idp-fixture") Project idpFixtureProject = xpackProject("test:idp-fixture")
evaluationDependsOn(idpFixtureProject.path) evaluationDependsOn(idpFixtureProject.path)
apply plugin: 'elasticsearch.standalone-test'
apply plugin: 'elasticsearch.vagrantsupport' apply plugin: 'elasticsearch.vagrantsupport'
apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
dependencies { dependencies {
testCompile project(path: xpackModule('core'), configuration: 'runtime') testCompile project(path: xpackModule('core'), configuration: 'runtime')
@ -11,35 +12,59 @@ dependencies {
testCompile 'com.google.jimfs:jimfs:1.1' testCompile 'com.google.jimfs:jimfs:1.1'
} }
processTestResources {
if (project.rootProject.vagrantSupported) {
dependsOn "idpFixture"
}
}
sourceSets {
test {
resources {
srcDirs += idpFixtureProject.file("src/main/resources/provision/generated")
srcDirs += project(xpackModule('security')).file('src/test/resources')
}
}
}
task idpFixture { task idpFixture {
dependsOn "vagrantCheckVersion", "virtualboxCheckVersion", idpFixtureProject.up dependsOn "vagrantCheckVersion", "virtualboxCheckVersion", idpFixtureProject.up
} }
String outputDir = "generated-resources/${project.name}"
task copyIdpCertificate(type: Copy) {
dependsOn idpFixture
from idpFixtureProject.file('src/main/resources/provision/generated/ca_server.pem');
into outputDir
}
if (project.rootProject.vagrantSupported) { if (project.rootProject.vagrantSupported) {
test.dependsOn idpFixture project.sourceSets.test.output.dir(outputDir, builtBy: copyIdpCertificate)
test.finalizedBy idpFixtureProject.halt integTestCluster.dependsOn idpFixture, copyIdpCertificate
integTest.finalizedBy idpFixtureProject.halt
} else { } else {
test.enabled = false integTest.enabled = false
} }
namingConventions { integTestCluster {
// integ tests use Tests instead of IT plugin ':x-pack-elasticsearch:plugin'
skipIntegTestInDisguise = true
setting 'xpack.security.http.ssl.enabled', 'false'
setting 'xpack.security.authc.token.enabled', 'true'
setting 'xpack.security.authc.realms.file.type', 'file'
setting 'xpack.security.authc.realms.file.order', '0'
setting 'xpack.security.authc.realms.shibboleth.type', 'saml'
setting 'xpack.security.authc.realms.shibboleth.order', '1'
setting 'xpack.security.authc.realms.shibboleth.idp.entity_id', 'https://test.shibboleth.elastic.local/'
setting 'xpack.security.authc.realms.shibboleth.idp.metadata.path', 'idp-metadata.xml'
setting 'xpack.security.authc.realms.shibboleth.sp.entity_id', 'http://mock.http.elastic.local/'
// The port in the ACS URL is fake - the test will bind the mock webserver
// to a random port and then whenever it needs to connect to a URL on the
// mock webserver it will replace 54321 with the real port
setting 'xpack.security.authc.realms.shibboleth.sp.acs', 'http://localhost:54321/saml/acs'
setting 'xpack.security.authc.realms.shibboleth.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.shibboleth.attributes.name', 'urn:oid:2.5.4.3'
setting 'xpack.ml.enabled', 'false'
extraConfigFile 'idp-metadata.xml', idpFixtureProject.file("src/main/resources/provision/generated/idp-metadata.xml")
setupCommand 'setupTestAdmin',
'bin/x-pack/users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser"
waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
dest: tmpFile.toString(),
username: 'test_admin',
password: 'x-pack-test-password',
ignoreerrors: true,
retries: 10)
return tmpFile.exists()
}
} }
forbiddenPatterns { forbiddenPatterns {
@ -61,7 +86,3 @@ thirdPartyAudit.excludes = [
'com.ibm.icu.lang.UCharacter' 'com.ibm.icu.lang.UCharacter'
] ]
test {
systemProperty 'es.set.netty.runtime.available.processors', 'false'
include '**/*Tests.class'
}

View File

@ -43,28 +43,17 @@ import org.elasticsearch.client.Response;
import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.lease.Releasables; import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.network.NetworkModule;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.mocksocket.MockHttpServer; import org.elasticsearch.mocksocket.MockHttpServer;
import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.test.SecuritySettingsSourceField;
import org.elasticsearch.xpack.core.common.socket.SocketAccess; import org.elasticsearch.xpack.core.common.socket.SocketAccess;
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingAction;
import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequestBuilder;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder;
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionParser;
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
import org.elasticsearch.xpack.core.ssl.CertUtils; import org.elasticsearch.xpack.core.ssl.CertUtils;
import org.elasticsearch.xpack.security.authc.Realms;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
@ -97,6 +86,7 @@ import java.util.regex.Pattern;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap; import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap;
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThan;
@ -109,25 +99,16 @@ import static org.hamcrest.Matchers.startsWith;
* An integration test for validating SAML authentication against a real Identity Provider (Shibboleth) * An integration test for validating SAML authentication against a real Identity Provider (Shibboleth)
*/ */
@SuppressForbidden(reason = "uses sun http server") @SuppressForbidden(reason = "uses sun http server")
public class SamlAuthenticationIntegTests extends SecurityIntegTestCase { public class SamlAuthenticationIT extends ESRestTestCase {
public static final String SP_LOGIN_PATH = "/saml/login"; private static final String SP_LOGIN_PATH = "/saml/login";
public static final String SP_ACS_PATH = "/saml/acs"; private static final String SP_ACS_PATH = "/saml/acs";
public static final String SAML_RESPONSE_FIELD = "SAMLResponse"; private static final String SAML_RESPONSE_FIELD = "SAMLResponse";
public static final String REQUEST_ID_COOKIE = "saml-request-id"; private static final String REQUEST_ID_COOKIE = "saml-request-id";
private static final String KIBANA_PASSWORD = "K1b@na K1b@na K1b@na";
private static HttpServer httpServer; private static HttpServer httpServer;
@BeforeClass
public static void setupSaml() throws Exception {
SamlTestCase.setupSaml();
}
@AfterClass
public static void cleanupSaml() throws Exception {
SamlTestCase.restoreLocale();
}
@BeforeClass @BeforeClass
public static void setupHttpServer() throws IOException { public static void setupHttpServer() throws IOException {
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0); InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0);
@ -180,48 +161,23 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
} }
@Override @Override
protected Settings nodeSettings(int nodeOrdinal) { protected Settings restAdminSettings() {
Settings.Builder builder = Settings.builder() String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
.put(super.nodeSettings(nodeOrdinal)) return Settings.builder()
.put(NetworkModule.HTTP_ENABLED.getKey(), true) .put(ThreadContext.PREFIX + ".Authorization", token)
.put("xpack.security.http.ssl.enabled", false) .build();
.put("xpack.security.authc.token.enabled", true)
.put("xpack.security.authc.realms.file.type", FileRealmSettings.TYPE)
.put("xpack.security.authc.realms.file.order", "0")
.put("xpack.security.authc.realms.shibboleth.type", SamlRealmSettings.TYPE)
.put("xpack.security.authc.realms.shibboleth.order", "1")
.put("xpack.security.authc.realms.shibboleth.idp.entity_id", "https://test.shibboleth.elastic.local/")
.put("xpack.security.authc.realms.shibboleth.idp.metadata.path", getDataPath("/idp-metadata.xml"))
.put("xpack.security.authc.realms.shibboleth.sp.entity_id", "http://mock.http.elastic.local/")
.put("xpack.security.authc.realms.shibboleth.sp.acs", getUrl(SP_ACS_PATH))
.put("xpack.security.authc.realms.shibboleth.attributes.principal", "uid")
.put("xpack.security.authc.realms.shibboleth.attributes.name", "urn:oid:2.5.4.3");
return builder.build();
}
@After
public void cleanupSecurity() {
for (Realms realms : internalCluster().getInstances(Realms.class)) {
realms.stream()
.filter(SamlRealm.class::isInstance)
.map(SamlRealm.class::cast)
.forEach(r -> Releasables.closeWhileHandlingException(r));
}
deleteSecurityIndex();
} }
/** /**
* We perform all requests to Elasticsearch as the "kibana" user, as this is the user that will be used * We perform all requests to Elasticsearch as the "kibana" user, as this is the user that will be used
* in a typical SAML deployment (where Kibana is providing the UI for the SAML Web SSO interacttions). * in a typical SAML deployment (where Kibana is providing the UI for the SAML Web SSO interactions).
* Before we can use the Kibana user, we need to set its password to something we know. * Before we can use the Kibana user, we need to set its password to something we know.
*/ */
@Before @Before
public void setKibanaPassword() throws IOException { public void setKibanaPassword() throws IOException {
new ChangePasswordRequestBuilder(client()) final HttpEntity json = new StringEntity("{ \"password\" : \"" + KIBANA_PASSWORD + "\" }", ContentType.APPLICATION_JSON);
.username("kibana") final Response response = adminClient().performRequest("PUT", "/_xpack/security/user/kibana/_password", emptyMap(), json);
.password(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()) assertOK(response);
.get();
} }
/** /**
@ -231,28 +187,21 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
*/ */
@Before @Before
public void setupRoleMapping() throws IOException { public void setupRoleMapping() throws IOException {
final String json = XContentBuilder.builder(XContentType.JSON.xContent()) final StringEntity json = new StringEntity(XContentBuilder.builder(XContentType.JSON.xContent())
.startObject() .startObject()
.array("roles", new String[] { "kibana_user"} )
.field("enabled", true)
.startObject("rules")
.startArray("all") .startArray("all")
.startObject() .startObject().startObject("field").field("username", "thor").endObject().endObject()
.startObject("field").field("username", "thor").endObject() .startObject().startObject("field").field("realm.name", "shibboleth").endObject().endObject()
.endObject() .endArray() // "all"
.startObject() .endObject() // "rules"
.startObject("field").field("realm.name", "shibboleth").endObject() .endObject() // top-level
.endObject() .string(), ContentType.APPLICATION_JSON);
.endArray()
.endObject() final Response response = adminClient().performRequest("PUT", "/_xpack/security/role_mapping/thor-kibana", emptyMap(), json);
.string(); assertOK(response);
final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY;
try (XContentParser parser = XContentType.JSON.xContent().createParser(registry, json)) {
final RoleMapperExpression expression = ExpressionParser.parseObject(parser, "thor-kibana");
new PutRoleMappingRequestBuilder(client(), PutRoleMappingAction.INSTANCE)
.enabled(true)
.name("thor-kibana")
.expression(expression)
.roles("kibana_user")
.get();
}
} }
/** /**
@ -302,8 +251,8 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
*/ */
private void verifyElasticsearchAccessToken(String accessToken) throws IOException { private void verifyElasticsearchAccessToken(String accessToken) throws IOException {
final BasicHeader authorization = new BasicHeader("Authorization", "Bearer " + accessToken); final BasicHeader authorization = new BasicHeader("Authorization", "Bearer " + accessToken);
final Response response = getRestClient().performRequest("GET", "/_xpack/security/_authenticate", authorization); final Response response = client().performRequest("GET", "/_xpack/security/_authenticate", authorization);
assertHttpOk(response); assertOK(response);
final Map<String, Object> map = parseResponseAsMap(response.getEntity()); final Map<String, Object> map = parseResponseAsMap(response.getEntity());
assertThat(map.get("username"), equalTo("thor")); assertThat(map.get("username"), equalTo("thor"));
assertThat(map.get("full_name"), equalTo("Thor Odinson")); assertThat(map.get("full_name"), equalTo("Thor Odinson"));
@ -323,9 +272,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
*/ */
private void verifyElasticsearchRefreshToken(String refreshToken) throws IOException { private void verifyElasticsearchRefreshToken(String refreshToken) throws IOException {
final String body = "{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }"; final String body = "{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }";
final Response response = getRestClient().performRequest("POST", "/_xpack/security/oauth2/token", final Response response = client().performRequest("POST", "/_xpack/security/oauth2/token",
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), authHeader("kibana")); emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth());
assertHttpOk(response); assertOK(response);
final Map<String, Object> result = parseResponseAsMap(response.getEntity()); final Map<String, Object> result = parseResponseAsMap(response.getEntity());
final Object newRefreshToken = result.get("refresh_token"); final Object newRefreshToken = result.get("refresh_token");
@ -416,7 +365,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
assertThat(acs.getPath(), equalTo(SP_ACS_PATH)); assertThat(acs.getPath(), equalTo(SP_ACS_PATH));
assertThat("SAML submission content", saml, notNullValue()); assertThat("SAML submission content", saml, notNullValue());
final HttpPost form = new HttpPost(acs); // The ACS url provided from the SP is going to be wrong because the gradle
// build doesn't know what the web server's port is, so it uses a fake one.
final HttpPost form = new HttpPost(getUrl(SP_ACS_PATH));
List<NameValuePair> params = new ArrayList<>(); List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair(SAML_RESPONSE_FIELD, saml)); params.add(new BasicNameValuePair(SAML_RESPONSE_FIELD, saml));
form.setEntity(new UrlEncodedFormEntity(params)); form.setEntity(new UrlEncodedFormEntity(params));
@ -511,9 +462,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
* sends a redirect to that page. * sends a redirect to that page.
*/ */
private void httpLogin(HttpExchange http) throws IOException { private void httpLogin(HttpExchange http) throws IOException {
final Response prepare = getRestClient().performRequest("POST", "/_xpack/security/saml/prepare", final Response prepare = client().performRequest("POST", "/_xpack/security/saml/prepare",
emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON), authHeader("kibana")); emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON), kibanaAuth());
assertHttpOk(prepare); assertOK(prepare);
final Map<String, Object> body = parseResponseAsMap(prepare.getEntity()); final Map<String, Object> body = parseResponseAsMap(prepare.getEntity());
logger.info("Created SAML authentication request {}", body); logger.info("Created SAML authentication request {}", body);
http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + body.get("id")); http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + body.get("id"));
@ -529,7 +480,7 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
*/ */
private void httpAcs(HttpExchange http) throws IOException { private void httpAcs(HttpExchange http) throws IOException {
final Response saml = samlAuthenticate(http); final Response saml = samlAuthenticate(http);
assertHttpOk(saml); assertOK(saml);
final byte[] content = Streams.copyToString(new InputStreamReader(saml.getEntity().getContent())).getBytes(); final byte[] content = Streams.copyToString(new InputStreamReader(saml.getEntity().getContent())).getBytes();
http.getResponseHeaders().add("Content-Type", "application/json"); http.getResponseHeaders().add("Content-Type", "application/json");
http.sendResponseHeaders(200, content.length); http.sendResponseHeaders(200, content.length);
@ -553,8 +504,8 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
assertThat(id, notNullValue()); assertThat(id, notNullValue());
final String body = "{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }"; final String body = "{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }";
return getRestClient().performRequest("POST", "/_xpack/security/saml/authenticate", return client().performRequest("POST", "/_xpack/security/saml/authenticate",
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), authHeader("kibana")); emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth());
} }
private List<NameValuePair> parseRequestForm(HttpExchange http) throws IOException { private List<NameValuePair> parseRequestForm(HttpExchange http) throws IOException {
@ -581,10 +532,6 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
} }
} }
private void assertHttpOk(Response response) {
assertHttpOk(response.getStatusLine());
}
private void assertHttpOk(StatusLine status) { private void assertHttpOk(StatusLine status) {
assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(200)); assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(200));
} }
@ -594,10 +541,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
assertThat(((List<?>) value), contains(expectedElement)); assertThat(((List<?>) value), contains(expectedElement));
} }
private static BasicHeader authHeader(String userName) { private static BasicHeader kibanaAuth() {
return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, final String auth = UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray()));
UsernamePasswordToken.basicAuthHeaderValue(userName, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING) return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth);
);
} }
private CloseableHttpClient getHttpClient() throws Exception { private CloseableHttpClient getHttpClient() throws Exception {