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:
parent
36ff4185a7
commit
427da8de8b
|
@ -1,8 +1,9 @@
|
|||
Project idpFixtureProject = xpackProject("test:idp-fixture")
|
||||
evaluationDependsOn(idpFixtureProject.path)
|
||||
|
||||
apply plugin: 'elasticsearch.standalone-test'
|
||||
apply plugin: 'elasticsearch.vagrantsupport'
|
||||
apply plugin: 'elasticsearch.standalone-rest-test'
|
||||
apply plugin: 'elasticsearch.rest-test'
|
||||
|
||||
dependencies {
|
||||
testCompile project(path: xpackModule('core'), configuration: 'runtime')
|
||||
|
@ -11,35 +12,59 @@ dependencies {
|
|||
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 {
|
||||
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) {
|
||||
test.dependsOn idpFixture
|
||||
test.finalizedBy idpFixtureProject.halt
|
||||
project.sourceSets.test.output.dir(outputDir, builtBy: copyIdpCertificate)
|
||||
integTestCluster.dependsOn idpFixture, copyIdpCertificate
|
||||
integTest.finalizedBy idpFixtureProject.halt
|
||||
} else {
|
||||
test.enabled = false
|
||||
integTest.enabled = false
|
||||
}
|
||||
|
||||
namingConventions {
|
||||
// integ tests use Tests instead of IT
|
||||
skipIntegTestInDisguise = true
|
||||
integTestCluster {
|
||||
plugin ':x-pack-elasticsearch:plugin'
|
||||
|
||||
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 {
|
||||
|
@ -61,7 +86,3 @@ thirdPartyAudit.excludes = [
|
|||
'com.ibm.icu.lang.UCharacter'
|
||||
]
|
||||
|
||||
test {
|
||||
systemProperty 'es.set.netty.runtime.available.processors', 'false'
|
||||
include '**/*Tests.class'
|
||||
}
|
||||
|
|
|
@ -43,28 +43,17 @@ import org.elasticsearch.client.Response;
|
|||
import org.elasticsearch.common.CheckedFunction;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.lease.Releasables;
|
||||
import org.elasticsearch.common.network.NetworkModule;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
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.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.mocksocket.MockHttpServer;
|
||||
import org.elasticsearch.test.SecurityIntegTestCase;
|
||||
import org.elasticsearch.test.SecuritySettingsSourceField;
|
||||
import org.elasticsearch.test.rest.ESRestTestCase;
|
||||
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.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.security.authc.Realms;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
|
@ -97,6 +86,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
import static java.util.Collections.emptyMap;
|
||||
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.equalTo;
|
||||
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)
|
||||
*/
|
||||
@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";
|
||||
public static final String SP_ACS_PATH = "/saml/acs";
|
||||
public static final String SAML_RESPONSE_FIELD = "SAMLResponse";
|
||||
public static final String REQUEST_ID_COOKIE = "saml-request-id";
|
||||
private static final String SP_LOGIN_PATH = "/saml/login";
|
||||
private static final String SP_ACS_PATH = "/saml/acs";
|
||||
private static final String SAML_RESPONSE_FIELD = "SAMLResponse";
|
||||
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;
|
||||
|
||||
|
||||
@BeforeClass
|
||||
public static void setupSaml() throws Exception {
|
||||
SamlTestCase.setupSaml();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanupSaml() throws Exception {
|
||||
SamlTestCase.restoreLocale();
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setupHttpServer() throws IOException {
|
||||
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0);
|
||||
|
@ -180,48 +161,23 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
Settings.Builder builder = Settings.builder()
|
||||
.put(super.nodeSettings(nodeOrdinal))
|
||||
.put(NetworkModule.HTTP_ENABLED.getKey(), true)
|
||||
.put("xpack.security.http.ssl.enabled", false)
|
||||
.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();
|
||||
protected Settings restAdminSettings() {
|
||||
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
|
||||
return Settings.builder()
|
||||
.put(ThreadContext.PREFIX + ".Authorization", token)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void setKibanaPassword() throws IOException {
|
||||
new ChangePasswordRequestBuilder(client())
|
||||
.username("kibana")
|
||||
.password(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray())
|
||||
.get();
|
||||
final HttpEntity json = new StringEntity("{ \"password\" : \"" + KIBANA_PASSWORD + "\" }", ContentType.APPLICATION_JSON);
|
||||
final Response response = adminClient().performRequest("PUT", "/_xpack/security/user/kibana/_password", emptyMap(), json);
|
||||
assertOK(response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,28 +187,21 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
*/
|
||||
@Before
|
||||
public void setupRoleMapping() throws IOException {
|
||||
final String json = XContentBuilder.builder(XContentType.JSON.xContent())
|
||||
final StringEntity json = new StringEntity(XContentBuilder.builder(XContentType.JSON.xContent())
|
||||
.startObject()
|
||||
.array("roles", new String[] { "kibana_user"} )
|
||||
.field("enabled", true)
|
||||
.startObject("rules")
|
||||
.startArray("all")
|
||||
.startObject()
|
||||
.startObject("field").field("username", "thor").endObject()
|
||||
.endObject()
|
||||
.startObject()
|
||||
.startObject("field").field("realm.name", "shibboleth").endObject()
|
||||
.endObject()
|
||||
.endArray()
|
||||
.endObject()
|
||||
.string();
|
||||
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();
|
||||
}
|
||||
.startObject().startObject("field").field("username", "thor").endObject().endObject()
|
||||
.startObject().startObject("field").field("realm.name", "shibboleth").endObject().endObject()
|
||||
.endArray() // "all"
|
||||
.endObject() // "rules"
|
||||
.endObject() // top-level
|
||||
.string(), ContentType.APPLICATION_JSON);
|
||||
|
||||
final Response response = adminClient().performRequest("PUT", "/_xpack/security/role_mapping/thor-kibana", emptyMap(), json);
|
||||
assertOK(response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -302,8 +251,8 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
*/
|
||||
private void verifyElasticsearchAccessToken(String accessToken) throws IOException {
|
||||
final BasicHeader authorization = new BasicHeader("Authorization", "Bearer " + accessToken);
|
||||
final Response response = getRestClient().performRequest("GET", "/_xpack/security/_authenticate", authorization);
|
||||
assertHttpOk(response);
|
||||
final Response response = client().performRequest("GET", "/_xpack/security/_authenticate", authorization);
|
||||
assertOK(response);
|
||||
final Map<String, Object> map = parseResponseAsMap(response.getEntity());
|
||||
assertThat(map.get("username"), equalTo("thor"));
|
||||
assertThat(map.get("full_name"), equalTo("Thor Odinson"));
|
||||
|
@ -323,9 +272,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
*/
|
||||
private void verifyElasticsearchRefreshToken(String refreshToken) throws IOException {
|
||||
final String body = "{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }";
|
||||
final Response response = getRestClient().performRequest("POST", "/_xpack/security/oauth2/token",
|
||||
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), authHeader("kibana"));
|
||||
assertHttpOk(response);
|
||||
final Response response = client().performRequest("POST", "/_xpack/security/oauth2/token",
|
||||
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth());
|
||||
assertOK(response);
|
||||
|
||||
final Map<String, Object> result = parseResponseAsMap(response.getEntity());
|
||||
final Object newRefreshToken = result.get("refresh_token");
|
||||
|
@ -416,7 +365,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
assertThat(acs.getPath(), equalTo(SP_ACS_PATH));
|
||||
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<>();
|
||||
params.add(new BasicNameValuePair(SAML_RESPONSE_FIELD, saml));
|
||||
form.setEntity(new UrlEncodedFormEntity(params));
|
||||
|
@ -511,9 +462,9 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
* sends a redirect to that page.
|
||||
*/
|
||||
private void httpLogin(HttpExchange http) throws IOException {
|
||||
final Response prepare = getRestClient().performRequest("POST", "/_xpack/security/saml/prepare",
|
||||
emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON), authHeader("kibana"));
|
||||
assertHttpOk(prepare);
|
||||
final Response prepare = client().performRequest("POST", "/_xpack/security/saml/prepare",
|
||||
emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON), kibanaAuth());
|
||||
assertOK(prepare);
|
||||
final Map<String, Object> body = parseResponseAsMap(prepare.getEntity());
|
||||
logger.info("Created SAML authentication request {}", body);
|
||||
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 {
|
||||
final Response saml = samlAuthenticate(http);
|
||||
assertHttpOk(saml);
|
||||
assertOK(saml);
|
||||
final byte[] content = Streams.copyToString(new InputStreamReader(saml.getEntity().getContent())).getBytes();
|
||||
http.getResponseHeaders().add("Content-Type", "application/json");
|
||||
http.sendResponseHeaders(200, content.length);
|
||||
|
@ -553,8 +504,8 @@ public class SamlAuthenticationIntegTests extends SecurityIntegTestCase {
|
|||
assertThat(id, notNullValue());
|
||||
|
||||
final String body = "{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }";
|
||||
return getRestClient().performRequest("POST", "/_xpack/security/saml/authenticate",
|
||||
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), authHeader("kibana"));
|
||||
return client().performRequest("POST", "/_xpack/security/saml/authenticate",
|
||||
emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth());
|
||||
}
|
||||
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
private static BasicHeader authHeader(String userName) {
|
||||
return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER,
|
||||
UsernamePasswordToken.basicAuthHeaderValue(userName, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)
|
||||
);
|
||||
private static BasicHeader kibanaAuth() {
|
||||
final String auth = UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray()));
|
||||
return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth);
|
||||
}
|
||||
|
||||
private CloseableHttpClient getHttpClient() throws Exception {
|
Loading…
Reference in New Issue