-
Notifications
You must be signed in to change notification settings - Fork 25k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Kerberos] Rest client integration test (#32070)
This commit adds the rest client integration test for Kerberos. This uses existing krb5kdc-fixture, which makes use of MIT Kerberos. Added support to create principals with password in krb5kdc-fixture. The rest test demonstrates the following: - Use of rest client to invoke Elasticsearch APIs authenticating using spnego mechanism, example showing what customizations we need to do to build the rest client. - test for login by keytab for user principal - test for login by username password for user principal
- Loading branch information
Showing
5 changed files
with
616 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import java.nio.file.Path | ||
import java.nio.file.Paths | ||
import java.nio.file.Files | ||
|
||
apply plugin: 'elasticsearch.vagrantsupport' | ||
apply plugin: 'elasticsearch.standalone-rest-test' | ||
apply plugin: 'elasticsearch.rest-test' | ||
|
||
dependencies { | ||
testCompile project(path: xpackModule('core'), configuration: 'runtime') | ||
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') | ||
testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') | ||
} | ||
|
||
// MIT Kerberos Vagrant Testing Fixture | ||
String box = "krb5kdc" | ||
Map<String,String> vagrantEnvVars = [ | ||
'VAGRANT_CWD' : "${project(':test:fixtures:krb5kdc-fixture').projectDir}", | ||
'VAGRANT_VAGRANTFILE' : 'Vagrantfile', | ||
'VAGRANT_PROJECT_DIR' : "${project(':test:fixtures:krb5kdc-fixture').projectDir}" | ||
] | ||
|
||
task krb5kdcUpdate(type: org.elasticsearch.gradle.vagrant.VagrantCommandTask) { | ||
command 'box' | ||
subcommand 'update' | ||
boxName box | ||
environmentVars vagrantEnvVars | ||
dependsOn "vagrantCheckVersion", "virtualboxCheckVersion" | ||
} | ||
|
||
task krb5kdcFixture(type: org.elasticsearch.gradle.test.VagrantFixture) { | ||
command 'up' | ||
args '--provision', '--provider', 'virtualbox' | ||
boxName box | ||
environmentVars vagrantEnvVars | ||
dependsOn krb5kdcUpdate | ||
} | ||
|
||
task krb5AddPrincipals { dependsOn krb5kdcFixture } | ||
|
||
List<String> principals = [ | ||
"HTTP/localhost", | ||
"peppa", | ||
"george:dino" | ||
] | ||
String realm = "BUILD.ELASTIC.CO" | ||
|
||
for (String principal : principals) { | ||
String[] princPwdPair = principal.split(':'); | ||
String princName = princPwdPair[0]; | ||
String password = ""; | ||
if (princPwdPair.length > 1) { | ||
password = princPwdPair[1]; | ||
} | ||
Task create = project.tasks.create("addPrincipal#${principal}".replace('/', '_'), org.elasticsearch.gradle.vagrant.VagrantCommandTask) { | ||
command 'ssh' | ||
args '--command', "sudo bash /vagrant/src/main/resources/provision/addprinc.sh $princName $password" | ||
boxName box | ||
environmentVars vagrantEnvVars | ||
dependsOn krb5kdcFixture | ||
} | ||
krb5AddPrincipals.dependsOn(create) | ||
} | ||
|
||
def generatedResources = "$buildDir/generated-resources/keytabs" | ||
task copyKeytabToGeneratedResources(type: Copy) { | ||
Path peppaKeytab = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("keytabs").resolve("peppa.keytab").toAbsolutePath() | ||
from peppaKeytab; | ||
into generatedResources | ||
} | ||
|
||
integTestCluster { | ||
setting 'xpack.license.self_generated.type', 'trial' | ||
setting 'xpack.security.enabled', 'true' | ||
setting 'xpack.security.authc.realms.file.type', 'file' | ||
setting 'xpack.security.authc.realms.file.order', '0' | ||
setting 'xpack.ml.enabled', 'false' | ||
setting 'xpack.security.audit.enabled', 'true' | ||
// Kerberos realm | ||
setting 'xpack.security.authc.realms.kerberos.type', 'kerberos' | ||
setting 'xpack.security.authc.realms.kerberos.order', '1' | ||
setting 'xpack.security.authc.realms.kerberos.keytab.path', 'es.keytab' | ||
setting 'xpack.security.authc.realms.kerberos.krb.debug', 'true' | ||
setting 'xpack.security.authc.realms.kerberos.remove_realm_name', 'false' | ||
|
||
Path krb5conf = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("conf").resolve("krb5.conf").toAbsolutePath() | ||
String jvmArgsStr = " -Djava.security.krb5.conf=${krb5conf}" + " -Dsun.security.krb5.debug=true" | ||
jvmArgs jvmArgsStr | ||
Path esKeytab = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("keytabs").resolve("HTTP_localhost.keytab").toAbsolutePath() | ||
extraConfigFile("es.keytab", "${esKeytab}") | ||
|
||
setupCommand 'setupTestAdmin', | ||
'bin/elasticsearch-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() | ||
} | ||
|
||
} | ||
|
||
integTestRunner { | ||
Path peppaKeytab = Paths.get("${project.buildDir}", "generated-resources", "keytabs", "peppa.keytab") | ||
systemProperty 'test.userkt', "peppa@${realm}" | ||
systemProperty 'test.userkt.keytab', "${peppaKeytab}" | ||
systemProperty 'test.userpwd', "george@${realm}" | ||
systemProperty 'test.userpwd.password', "dino" | ||
systemProperty 'tests.security.manager', 'true' | ||
Path krb5conf = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("conf").resolve("krb5.conf").toAbsolutePath() | ||
List jvmargs = ["-Djava.security.krb5.conf=${krb5conf}","-Dsun.security.krb5.debug=true"] | ||
jvmArgs jvmargs | ||
} | ||
|
||
if (project.rootProject.vagrantSupported == false) { | ||
integTest.enabled = false | ||
} else { | ||
project.sourceSets.test.output.dir(generatedResources, builtBy: copyKeytabToGeneratedResources) | ||
integTestCluster.dependsOn krb5AddPrincipals, krb5kdcFixture, copyKeytabToGeneratedResources | ||
integTest.finalizedBy project(':test:fixtures:krb5kdc-fixture').halt | ||
} |
152 changes: 152 additions & 0 deletions
152
...c/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/* | ||
* 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.kerberos; | ||
|
||
import org.apache.http.HttpEntity; | ||
import org.apache.http.HttpHost; | ||
import org.elasticsearch.client.Request; | ||
import org.elasticsearch.client.Response; | ||
import org.elasticsearch.client.RestClient; | ||
import org.elasticsearch.client.RestClientBuilder; | ||
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.settings.SecureString; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.unit.TimeValue; | ||
import org.elasticsearch.common.util.concurrent.ThreadContext; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.XContentType; | ||
import org.elasticsearch.test.rest.ESRestTestCase; | ||
import org.junit.Before; | ||
|
||
import java.io.IOException; | ||
import java.security.AccessControlContext; | ||
import java.security.AccessController; | ||
import java.security.PrivilegedActionException; | ||
import java.security.PrivilegedExceptionAction; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import javax.security.auth.login.LoginContext; | ||
|
||
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.instanceOf; | ||
|
||
/** | ||
* Integration test to demonstrate authentication against a real MIT Kerberos | ||
* instance. | ||
* <p> | ||
* Demonstrates login by keytab and login by password for given user principal | ||
* name using rest client. | ||
*/ | ||
public class KerberosAuthenticationIT extends ESRestTestCase { | ||
private static final String ENABLE_KERBEROS_DEBUG_LOGS_KEY = "test.krb.debug"; | ||
private static final String TEST_USER_WITH_KEYTAB_KEY = "test.userkt"; | ||
private static final String TEST_USER_WITH_KEYTAB_PATH_KEY = "test.userkt.keytab"; | ||
private static final String TEST_USER_WITH_PWD_KEY = "test.userpwd"; | ||
private static final String TEST_USER_WITH_PWD_PASSWD_KEY = "test.userpwd.password"; | ||
private static final String TEST_KERBEROS_REALM_NAME = "kerberos"; | ||
|
||
@Override | ||
protected Settings restAdminSettings() { | ||
final String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); | ||
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); | ||
} | ||
|
||
/** | ||
* Creates simple mapping that maps the users from 'kerberos' realm to | ||
* the 'kerb_test' role. | ||
*/ | ||
@Before | ||
public void setupRoleMapping() throws IOException { | ||
final String json = Strings // top-level | ||
.toString(XContentBuilder.builder(XContentType.JSON.xContent()).startObject() | ||
.array("roles", new String[] { "kerb_test" }) | ||
.field("enabled", true) | ||
.startObject("rules") | ||
.startArray("all") | ||
.startObject().startObject("field").field("realm.name", TEST_KERBEROS_REALM_NAME).endObject().endObject() | ||
.endArray() // "all" | ||
.endObject() // "rules" | ||
.endObject()); | ||
|
||
final Request request = new Request("POST", "/_xpack/security/role_mapping/kerberosrolemapping"); | ||
request.setJsonEntity(json); | ||
final Response response = adminClient().performRequest(request); | ||
assertOK(response); | ||
} | ||
|
||
public void testLoginByKeytab() throws IOException, PrivilegedActionException { | ||
final String userPrincipalName = System.getProperty(TEST_USER_WITH_KEYTAB_KEY); | ||
final String keytabPath = System.getProperty(TEST_USER_WITH_KEYTAB_PATH_KEY); | ||
final boolean enabledDebugLogs = Boolean.parseBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); | ||
final SpnegoHttpClientConfigCallbackHandler callbackHandler = new SpnegoHttpClientConfigCallbackHandler(userPrincipalName, | ||
keytabPath, enabledDebugLogs); | ||
executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); | ||
} | ||
|
||
public void testLoginByUsernamePassword() throws IOException, PrivilegedActionException { | ||
final String userPrincipalName = System.getProperty(TEST_USER_WITH_PWD_KEY); | ||
final String password = System.getProperty(TEST_USER_WITH_PWD_PASSWD_KEY); | ||
final boolean enabledDebugLogs = Boolean.parseBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); | ||
final SpnegoHttpClientConfigCallbackHandler callbackHandler = new SpnegoHttpClientConfigCallbackHandler(userPrincipalName, | ||
new SecureString(password.toCharArray()), enabledDebugLogs); | ||
executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); | ||
} | ||
|
||
private void executeRequestAndVerifyResponse(final String userPrincipalName, | ||
final SpnegoHttpClientConfigCallbackHandler callbackHandler) throws PrivilegedActionException, IOException { | ||
final Request request = new Request("GET", "/_xpack/security/_authenticate"); | ||
try (RestClient restClient = buildRestClientForKerberos(callbackHandler)) { | ||
final AccessControlContext accessControlContext = AccessController.getContext(); | ||
final LoginContext lc = callbackHandler.login(); | ||
Response response = SpnegoHttpClientConfigCallbackHandler.doAsPrivilegedWrapper(lc.getSubject(), | ||
(PrivilegedExceptionAction<Response>) () -> { | ||
return restClient.performRequest(request); | ||
}, accessControlContext); | ||
|
||
assertOK(response); | ||
final Map<String, Object> map = parseResponseAsMap(response.getEntity()); | ||
assertThat(map.get("username"), equalTo(userPrincipalName)); | ||
assertThat(map.get("roles"), instanceOf(List.class)); | ||
assertThat(((List<?>) map.get("roles")), contains("kerb_test")); | ||
} | ||
} | ||
|
||
private Map<String, Object> parseResponseAsMap(final HttpEntity entity) throws IOException { | ||
return convertToMap(XContentType.JSON.xContent(), entity.getContent(), false); | ||
} | ||
|
||
private RestClient buildRestClientForKerberos(final SpnegoHttpClientConfigCallbackHandler callbackHandler) throws IOException { | ||
final Settings settings = restAdminSettings(); | ||
final HttpHost[] hosts = getClusterHosts().toArray(new HttpHost[getClusterHosts().size()]); | ||
|
||
final RestClientBuilder restClientBuilder = RestClient.builder(hosts); | ||
configureRestClientBuilder(restClientBuilder, settings); | ||
restClientBuilder.setHttpClientConfigCallback(callbackHandler); | ||
return restClientBuilder.build(); | ||
} | ||
|
||
private static void configureRestClientBuilder(final RestClientBuilder restClientBuilder, final Settings settings) | ||
throws IOException { | ||
final String requestTimeoutString = settings.get(CLIENT_RETRY_TIMEOUT); | ||
if (requestTimeoutString != null) { | ||
final TimeValue maxRetryTimeout = TimeValue.parseTimeValue(requestTimeoutString, CLIENT_RETRY_TIMEOUT); | ||
restClientBuilder.setMaxRetryTimeoutMillis(Math.toIntExact(maxRetryTimeout.getMillis())); | ||
} | ||
final String socketTimeoutString = settings.get(CLIENT_SOCKET_TIMEOUT); | ||
if (socketTimeoutString != null) { | ||
final TimeValue socketTimeout = TimeValue.parseTimeValue(socketTimeoutString, CLIENT_SOCKET_TIMEOUT); | ||
restClientBuilder.setRequestConfigCallback(conf -> conf.setSocketTimeout(Math.toIntExact(socketTimeout.getMillis()))); | ||
} | ||
if (settings.hasValue(CLIENT_PATH_PREFIX)) { | ||
restClientBuilder.setPathPrefix(settings.get(CLIENT_PATH_PREFIX)); | ||
} | ||
} | ||
} |
Oops, something went wrong.