Skip to content

Commit

Permalink
[Kerberos] Rest client integration test (#32070)
Browse files Browse the repository at this point in the history
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
bizybot authored Jul 18, 2018
1 parent 45690fc commit dd7cdfd
Show file tree
Hide file tree
Showing 5 changed files with 616 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
set -e

if [[ $# -lt 1 ]]; then
echo 'Usage: addprinc.sh <principalNameNoRealm>'
echo 'Usage: addprinc.sh principalName [password]'
echo ' principalName user principal name without realm'
echo ' password If provided then will set password for user else it will provision user with keytab'
exit 1
fi

PRINC="$1"
PASSWD="$2"
USER=$(echo $PRINC | tr "/" "_")

VDIR=/vagrant
Expand All @@ -47,12 +50,17 @@ ADMIN_KTAB=$LOCALSTATEDIR/admin.keytab
USER_PRIN=$PRINC@$REALM
USER_KTAB=$LOCALSTATEDIR/$USER.keytab

if [ -f $USER_KTAB ]; then
if [ -f $USER_KTAB ] && [ -z "$PASSWD" ]; then
echo "Principal '${PRINC}@${REALM}' already exists. Re-copying keytab..."
sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab
else
echo "Provisioning '${PRINC}@${REALM}' principal and keytab..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -randkey $USER_PRIN"
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "ktadd -k $USER_KTAB $USER_PRIN"
if [ -z "$PASSWD" ]; then
echo "Provisioning '${PRINC}@${REALM}' principal and keytab..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -randkey $USER_PRIN"
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "ktadd -k $USER_KTAB $USER_PRIN"
sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab
else
echo "Provisioning '${PRINC}@${REALM}' principal with password..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -pw $PASSWD $PRINC"
fi
fi

sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab
126 changes: 126 additions & 0 deletions x-pack/qa/kerberos-tests/build.gradle
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
}
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));
}
}
}
Loading

0 comments on commit dd7cdfd

Please sign in to comment.