From de23f3f6e29274694b3f79d166a127b138efedea Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Sat, 14 Jul 2018 02:14:34 +1000 Subject: [PATCH 1/8] [Kerberos] Rest client integration test 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 --- .../src/main/resources/provision/addprinc.sh | 22 +- x-pack/qa/kerberos-tests/build.gradle | 114 ++++++ ...CustomHttpClientConfigCallbackHandler.java | 332 ++++++++++++++++++ .../kerberos/KerberosAuthenticationIT.java | 149 ++++++++ 4 files changed, 610 insertions(+), 7 deletions(-) create mode 100644 x-pack/qa/kerberos-tests/build.gradle create mode 100644 x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java create mode 100644 x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java diff --git a/test/fixtures/krb5kdc-fixture/src/main/resources/provision/addprinc.sh b/test/fixtures/krb5kdc-fixture/src/main/resources/provision/addprinc.sh index 137135dc2aa4d..d0d1570ae299a 100755 --- a/test/fixtures/krb5kdc-fixture/src/main/resources/provision/addprinc.sh +++ b/test/fixtures/krb5kdc-fixture/src/main/resources/provision/addprinc.sh @@ -20,11 +20,14 @@ set -e if [[ $# -lt 1 ]]; then - echo 'Usage: addprinc.sh ' + 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 @@ -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 diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle new file mode 100644 index 0000000000000..079cddcf44601 --- /dev/null +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -0,0 +1,114 @@ +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 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 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) +} + +integTestCluster { + setting 'xpack.license.self_generated.type', 'trial' + setting 'xpack.security.enabled', '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.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() + } + + dependsOn krb5AddPrincipals, krb5kdcFixture +} + +integTestRunner { + Path peppaKeytab = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("keytabs").resolve("peppa.keytab").toAbsolutePath() + systemProperty 'test.userkt', "peppa@${realm}" + systemProperty 'test.userkt.keytab', "${peppaKeytab}" + systemProperty 'test.userpwd', "george@${realm}" + systemProperty 'test.userpwd.password', "dino" + systemProperty 'tests.security.manager', 'false' + 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 +} diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java new file mode 100644 index 0000000000000..42097ff1a8a23 --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java @@ -0,0 +1,332 @@ +package org.elasticsearch.xpack.security.authc.kerberos; + +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.KerberosCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.config.Lookup; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; +import org.apache.http.ssl.SSLContexts; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback; +import org.elasticsearch.common.io.PathUtils; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** + * This class implements {@link HttpClientConfigCallback} which allows for + * customization of {@link HttpAsyncClientBuilder}. + *

+ * Based on the configuration, configures {@link HttpAsyncClientBuilder} to + * support spengo auth scheme. + *

+ * It uses {@link Settings} to load trust store and configures + * {@link HttpAsyncClientBuilder}.
+ * It uses configured credentials either password or keytab for authentication. + */ +public class CustomHttpClientConfigCallbackHandler implements HttpClientConfigCallback { + private static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + private static final String CRED_CONF_NAME = "ESClientLoginConf"; + private static final Oid SPNEGO_OID = getSpnegoOid(); + + private static Oid getSpnegoOid() { + Oid oid = null; + try { + oid = new Oid("1.3.6.1.5.5.2"); + } catch (GSSException gsse) { + throw ExceptionsHelper.convertToRuntime(gsse); + } + return oid; + } + + private final String userPrincipalName; + private final SecureString password; + private final String keytabPath; + private final Boolean enableDebugLogs; + private final Settings settings; + private LoginContext loginContext; + + /** + * Constructs {@link CustomHttpClientConfigCallbackHandler} with given + * principalName and password. + * + * @param userPrincipalName user principal name + * @param password password for user + * @param enableDebugLogs if {@code true} enables kerberos debug logs + * @param settings {@link Settings} + */ + public CustomHttpClientConfigCallbackHandler(final String userPrincipalName, final SecureString password, final Boolean enableDebugLogs, + final Settings settings) { + this.userPrincipalName = userPrincipalName; + this.password = password; + this.keytabPath = null; + this.enableDebugLogs = enableDebugLogs; + this.settings = settings; + } + + /** + * Constructs {@link CustomHttpClientConfigCallbackHandler} with given + * principalName and keytab. + * + * @param userPrincipalName User principal name + * @param keytabPath path to keytab file for user + */ + public CustomHttpClientConfigCallbackHandler(final String userPrincipalName, final String keytabPath, final Boolean enableDebugLogs, + final Settings settings) { + this.userPrincipalName = userPrincipalName; + this.keytabPath = keytabPath; + this.password = null; + this.enableDebugLogs = enableDebugLogs; + this.settings = settings; + } + + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + setupSpnegoAuthSchemeSupport(httpClientBuilder); + setupSSLSettings(httpClientBuilder); + return httpClientBuilder; + } + + private void setupSSLSettings(HttpAsyncClientBuilder httpClientBuilder) { + final String keystorePath = settings.get(ESRestTestCase.TRUSTSTORE_PATH); + if (keystorePath != null) { + final String keystorePass = settings.get(ESRestTestCase.TRUSTSTORE_PASSWORD); + if (keystorePass == null) { + throw new IllegalStateException( + ESRestTestCase.TRUSTSTORE_PATH + " is provided but not " + ESRestTestCase.TRUSTSTORE_PASSWORD); + } + final Path path = PathUtils.get(keystorePath); + if (!Files.exists(path)) { + throw new IllegalStateException(ESRestTestCase.TRUSTSTORE_PATH + " is set but points to a non-existing file"); + } + try (InputStream is = Files.newInputStream(path)) { + final KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load(is, keystorePass.toCharArray()); + final SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(keyStore, null).build(); + final SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(sslcontext); + httpClientBuilder.setSSLStrategy(sessionStrategy); + } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | CertificateException e) { + throw new RuntimeException("Error setting up ssl", e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private void setupSpnegoAuthSchemeSupport(HttpAsyncClientBuilder httpClientBuilder) { + final Lookup authSchemeRegistry = RegistryBuilder.create() + .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory()).build(); + + final GSSManager gssManager = GSSManager.getInstance(); + try { + final GSSName gssUserPrincipalName = gssManager.createName(userPrincipalName, GSSName.NT_USER_NAME); + login(); + final GSSCredential credential = Subject.doAs(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssManager.createCredential(gssUserPrincipalName, + GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); + + final KerberosCredentialsProvider credentialsProvider = new KerberosCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.SPNEGO), + new KerberosCredentials(credential)); + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } catch (GSSException e) { + throw new RuntimeException(e); + } catch (LoginException e) { + throw new RuntimeException(e); + } catch (PrivilegedActionException e) { + throw new RuntimeException(e); + } + httpClientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry); + } + + /** + * If logged in {@link LoginContext} is not available, it attempts login and + * returns {@link LoginContext} + * + * @return {@link LoginContext} + * @throws LoginException + */ + public LoginContext login() throws LoginException { + if (this.loginContext == null) { + final Subject subject = new Subject(false, Collections.singleton(new KerberosPrincipal(userPrincipalName)), + Collections.emptySet(), Collections.emptySet()); + Configuration conf = null; + final CallbackHandler callback; + if (password != null) { + conf = new PasswordJaasConf(userPrincipalName, enableDebugLogs); + callback = new KrbCallbackHandler(userPrincipalName, password); + } else { + conf = new KeytabJaasConf(userPrincipalName, keytabPath, enableDebugLogs); + callback = null; + } + loginContext = new LoginContext(CRED_CONF_NAME, subject, callback, conf); + loginContext.login(); + } + return loginContext; + } + + /** + * This class matches {@link AuthScope} and based on that returns + * {@link Credentials}. Only supports {@link AuthSchemes#SPNEGO} in + * {@link AuthScope#getScheme()} + */ + private static class KerberosCredentialsProvider implements CredentialsProvider { + private AuthScope authScope; + private Credentials credentials; + + @Override + public void setCredentials(AuthScope authscope, Credentials credentials) { + if (authscope.getScheme().regionMatches(true, 0, AuthSchemes.SPNEGO, 0, AuthSchemes.SPNEGO.length()) == false) { + throw new IllegalArgumentException("Only " + AuthSchemes.SPNEGO + " auth scheme is supported in AuthScope"); + } + this.authScope = authscope; + this.credentials = credentials; + } + + @Override + public Credentials getCredentials(AuthScope authscope) { + assert this.authScope != null && authscope != null; + return authscope.match(this.authScope) > -1 ? this.credentials : null; + } + + @Override + public void clear() { + this.authScope = null; + this.credentials = null; + } + } + + /** + * Jaas call back handler to provide credentials. + */ + private static class KrbCallbackHandler implements CallbackHandler { + private final String principal; + private final SecureString password; + + KrbCallbackHandler(final String principal, final SecureString password) { + this.principal = principal; + this.password = password; + } + + public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback pc = (PasswordCallback) callback; + if (pc.getPrompt().contains(principal)) { + pc.setPassword(password.getChars()); + break; + } + } + } + } + } + + /** + * Usually we would have a JAAS configuration file for login configuration. + * Instead of an additional file setting as we do not want the options to be + * customizable we are constructing it in memory. + *

+ * As we are using this instead of jaas.conf, this requires refresh of + * {@link Configuration} and reqires appropriate security permissions to do so. + */ + private static class PasswordJaasConf extends AbstractJaasConf { + + PasswordJaasConf(final String userPrincipalName, final Boolean enableDebugLogs) { + super(userPrincipalName, enableDebugLogs); + } + + public void addOptions(final Map options) { + options.put("useTicketCache", Boolean.FALSE.toString()); + options.put("useKeyTab", Boolean.FALSE.toString()); + } + } + + /** + * Usually we would have a JAAS configuration file for login configuration. As + * we have static configuration except debug flag, we are constructing in + * memory. This avoids additional configuration required from the user. + *

+ * As we are using this instead of jaas.conf, this requires refresh of + * {@link Configuration} and requires appropriate security permissions to do so. + */ + private static class KeytabJaasConf extends AbstractJaasConf { + private final String keytabFilePath; + + KeytabJaasConf(final String userPrincipalName, final String keytabFilePath, final Boolean enableDebugLogs) { + super(userPrincipalName, enableDebugLogs); + this.keytabFilePath = keytabFilePath; + } + + public void addOptions(final Map options) { + options.put("useKeyTab", Boolean.TRUE.toString()); + options.put("keyTab", keytabFilePath); + options.put("doNotPrompt", Boolean.TRUE.toString()); + } + + } + + private abstract static class AbstractJaasConf extends Configuration { + private final String userPrincipalName; + private final Boolean enableDebugLogs; + + AbstractJaasConf(final String userPrincipalName, final Boolean enableDebugLogs) { + this.userPrincipalName = userPrincipalName; + this.enableDebugLogs = enableDebugLogs; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final Map options = new HashMap<>(); + options.put("principal", userPrincipalName); + options.put("refreshKrb5Config", Boolean.TRUE.toString()); + options.put("isInitiator", Boolean.TRUE.toString()); + options.put("storeKey", Boolean.TRUE.toString()); + options.put("renewTGT", Boolean.FALSE.toString()); + options.put("debug", enableDebugLogs.toString()); + addOptions(options); + return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Collections.unmodifiableMap(options)) }; + } + + abstract void addOptions(Map options); + } +} diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java new file mode 100644 index 0000000000000..bace1cacd8d1f --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java @@ -0,0 +1,149 @@ +/* + * 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.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.List; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +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. + *

+ * 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_WTIH_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 'super_user' role. + */ + @Before + public void setupRoleMapping() throws IOException { + final String json = Strings // top-level + .toString(XContentBuilder.builder(XContentType.JSON.xContent()).startObject().array("roles", new String[] { "super_user" }) + .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, LoginException, 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.valueOf(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); + final CustomHttpClientConfigCallbackHandler callbackHandler = new CustomHttpClientConfigCallbackHandler(userPrincipalName, + keytabPath, enabledDebugLogs, restAdminSettings()); + executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); + } + + public void testLoginByUsernamePassword() throws IOException, LoginException, PrivilegedActionException { + final String userPrincipalName = System.getProperty(TEST_USER_WITH_PWD_KEY); + final String password = System.getProperty(TEST_USER_WTIH_PWD_PASSWD_KEY); + final Boolean enabledDebugLogs = Boolean.valueOf(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); + final CustomHttpClientConfigCallbackHandler callbackHandler = new CustomHttpClientConfigCallbackHandler(userPrincipalName, + new SecureString(password.toCharArray()), enabledDebugLogs, restAdminSettings()); + executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); + } + + private void executeRequestAndVerifyResponse(final String userPrincipalName, + final CustomHttpClientConfigCallbackHandler callbackHandler) throws LoginException, PrivilegedActionException, IOException { + final Request request = new Request("GET", "/_xpack/security/_authenticate"); + try (RestClient restClient = buildRestClientForKerberos(callbackHandler)) { + LoginContext lc = callbackHandler.login(); + final Response response = Subject.doAs(lc.getSubject(), + (PrivilegedExceptionAction) () -> KerberosAuthenticationIT.this.execute(restClient, request)); + + assertOK(response); + final Map map = parseResponseAsMap(response.getEntity()); + assertThat(map.get("username"), equalTo(userPrincipalName)); + assertThat(map.get("roles"), instanceOf(List.class)); + assertThat(((List) map.get("roles")), contains("super_user")); + } + } + + private Map parseResponseAsMap(final HttpEntity entity) throws IOException { + return convertToMap(XContentType.JSON.xContent(), entity.getContent(), false); + } + + private Response execute(final RestClient restClient, final Request request) throws IOException { + return restClient.performRequest(request); + } + + private RestClient buildRestClientForKerberos(final CustomHttpClientConfigCallbackHandler 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)); + } + } +} From b332be86b81fe1e5e137d7186a3e5cdad61a9af8 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Mon, 16 Jul 2018 17:43:26 +1000 Subject: [PATCH 2/8] [Kerberos] Address review comments. --- .../kerberos/KerberosAuthenticationIT.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java index bace1cacd8d1f..61e48abe1979d 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java @@ -50,7 +50,7 @@ public class KerberosAuthenticationIT extends ESRestTestCase { 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_WTIH_PWD_PASSWD_KEY = "test.userpwd.password"; + private static final String TEST_USER_WITH_PWD_PASSWD_KEY = "test.userpwd.password"; private static final String TEST_KERBEROS_REALM_NAME = "kerberos"; @Override @@ -61,14 +61,18 @@ protected Settings restAdminSettings() { /** * Creates simple mapping that maps the users from 'kerberos' realm to - * the 'super_user' role. + * 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[] { "super_user" }) - .field("enabled", true).startObject("rules").startArray("all").startObject().startObject("field") - .field("realm.name", TEST_KERBEROS_REALM_NAME).endObject().endObject().endArray() // "all" + .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()); @@ -89,7 +93,7 @@ public void testLoginByKeytab() throws IOException, LoginException, PrivilegedAc public void testLoginByUsernamePassword() throws IOException, LoginException, PrivilegedActionException { final String userPrincipalName = System.getProperty(TEST_USER_WITH_PWD_KEY); - final String password = System.getProperty(TEST_USER_WTIH_PWD_PASSWD_KEY); + final String password = System.getProperty(TEST_USER_WITH_PWD_PASSWD_KEY); final Boolean enabledDebugLogs = Boolean.valueOf(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); final CustomHttpClientConfigCallbackHandler callbackHandler = new CustomHttpClientConfigCallbackHandler(userPrincipalName, new SecureString(password.toCharArray()), enabledDebugLogs, restAdminSettings()); @@ -108,7 +112,7 @@ private void executeRequestAndVerifyResponse(final String userPrincipalName, final Map map = parseResponseAsMap(response.getEntity()); assertThat(map.get("username"), equalTo(userPrincipalName)); assertThat(map.get("roles"), instanceOf(List.class)); - assertThat(((List) map.get("roles")), contains("super_user")); + assertThat(((List) map.get("roles")), contains("kerb_test")); } } From 9b8d5ba8f4260ba1bba863ba2cd0c8e1f28aed3d Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Mon, 16 Jul 2018 21:04:03 +1000 Subject: [PATCH 3/8] [Kerberos] Check if vagrant is available else disable tests --- x-pack/qa/kerberos-tests/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index 079cddcf44601..eb799c393bd2b 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -112,3 +112,7 @@ integTestRunner { List jvmargs = ["-Djava.security.krb5.conf=${krb5conf}","-Dsun.security.krb5.debug=true"] jvmArgs jvmargs } + +if (project.rootProject.vagrantSupported == false) { + integTest.enabled = false +} \ No newline at end of file From e31628fdc3e8da828667561e5fd4691720abc9d5 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Mon, 16 Jul 2018 23:21:55 +1000 Subject: [PATCH 4/8] [Kerberos] Add missing license header --- .../kerberos/CustomHttpClientConfigCallbackHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java index 42097ff1a8a23..4187223d363d9 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java @@ -1,3 +1,9 @@ +/* + * 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.auth.AuthSchemeProvider; From 307d0a4a48d27a292bb83f9b427e1041764d9ca1 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 17 Jul 2018 15:12:40 +1000 Subject: [PATCH 5/8] [Kerberos] Address review comments - Enable security manager for tests and related modifications to give permissions - Rename HttpClientConfigCallbackHandler - Remove unwanted code. --- x-pack/qa/kerberos-tests/build.gradle | 4 +- .../kerberos/KerberosAuthenticationIT.java | 37 +++-- ...pnegoHttpClientConfigCallbackHandler.java} | 146 ++++++++---------- .../src/test/resources/plugin-security.policy | 5 + 4 files changed, 87 insertions(+), 105 deletions(-) rename x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{CustomHttpClientConfigCallbackHandler.java => SpnegoHttpClientConfigCallbackHandler.java} (69%) create mode 100644 x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index eb799c393bd2b..025239f7a7463 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -65,8 +65,6 @@ for (String principal : principals) { integTestCluster { setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.enabled', '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.ml.enabled', 'false' @@ -107,7 +105,7 @@ integTestRunner { systemProperty 'test.userkt.keytab', "${peppaKeytab}" systemProperty 'test.userpwd', "george@${realm}" systemProperty 'test.userpwd.password', "dino" - systemProperty 'tests.security.manager', 'false' + 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 diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java index 61e48abe1979d..189fb030177aa 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java @@ -23,14 +23,14 @@ 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.Subject; import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; @@ -82,31 +82,34 @@ public void setupRoleMapping() throws IOException { assertOK(response); } - public void testLoginByKeytab() throws IOException, LoginException, PrivilegedActionException { + 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.valueOf(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); - final CustomHttpClientConfigCallbackHandler callbackHandler = new CustomHttpClientConfigCallbackHandler(userPrincipalName, - keytabPath, enabledDebugLogs, restAdminSettings()); + final boolean enabledDebugLogs = Boolean.getBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); + final SpnegoHttpClientConfigCallbackHandler callbackHandler = new SpnegoHttpClientConfigCallbackHandler(userPrincipalName, + keytabPath, enabledDebugLogs); executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); } - public void testLoginByUsernamePassword() throws IOException, LoginException, PrivilegedActionException { + 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.valueOf(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); - final CustomHttpClientConfigCallbackHandler callbackHandler = new CustomHttpClientConfigCallbackHandler(userPrincipalName, - new SecureString(password.toCharArray()), enabledDebugLogs, restAdminSettings()); + final boolean enabledDebugLogs = Boolean.getBoolean(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 CustomHttpClientConfigCallbackHandler callbackHandler) throws LoginException, PrivilegedActionException, IOException { + final SpnegoHttpClientConfigCallbackHandler callbackHandler) throws PrivilegedActionException, IOException { final Request request = new Request("GET", "/_xpack/security/_authenticate"); try (RestClient restClient = buildRestClientForKerberos(callbackHandler)) { - LoginContext lc = callbackHandler.login(); - final Response response = Subject.doAs(lc.getSubject(), - (PrivilegedExceptionAction) () -> KerberosAuthenticationIT.this.execute(restClient, request)); + final AccessControlContext accessControlContext = AccessController.getContext(); + final LoginContext lc = callbackHandler.login(); + Response response = SpnegoHttpClientConfigCallbackHandler.doAsPrivilegedWrapper(lc.getSubject(), + (PrivilegedExceptionAction) () -> { + return restClient.performRequest(request); + }, accessControlContext); assertOK(response); final Map map = parseResponseAsMap(response.getEntity()); @@ -120,11 +123,7 @@ private Map parseResponseAsMap(final HttpEntity entity) throws I return convertToMap(XContentType.JSON.xContent(), entity.getContent(), false); } - private Response execute(final RestClient restClient, final Request request) throws IOException { - return restClient.performRequest(request); - } - - private RestClient buildRestClientForKerberos(final CustomHttpClientConfigCallbackHandler callbackHandler) throws IOException { + private RestClient buildRestClientForKerberos(final SpnegoHttpClientConfigCallbackHandler callbackHandler) throws IOException { final Settings settings = restAdminSettings(); final HttpHost[] hosts = getClusterHosts().toArray(new HttpHost[getClusterHosts().size()]); diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java similarity index 69% rename from x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java rename to x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java index 4187223d363d9..4c87a8057a33e 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/CustomHttpClientConfigCallbackHandler.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java @@ -16,14 +16,9 @@ import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.auth.SPNegoSchemeFactory; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; -import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; -import org.apache.http.ssl.SSLContexts; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback; -import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.rest.ESRestTestCase; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; @@ -31,21 +26,14 @@ import org.ietf.jgss.Oid; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; +import java.security.AccessControlContext; +import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import javax.net.ssl.SSLContext; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -55,20 +43,16 @@ import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; /** * This class implements {@link HttpClientConfigCallback} which allows for * customization of {@link HttpAsyncClientBuilder}. *

* Based on the configuration, configures {@link HttpAsyncClientBuilder} to - * support spengo auth scheme. - *

- * It uses {@link Settings} to load trust store and configures - * {@link HttpAsyncClientBuilder}.
+ * support spengo auth scheme.
* It uses configured credentials either password or keytab for authentication. */ -public class CustomHttpClientConfigCallbackHandler implements HttpClientConfigCallback { +public class SpnegoHttpClientConfigCallbackHandler implements HttpClientConfigCallback { private static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; private static final String CRED_CONF_NAME = "ESClientLoginConf"; private static final Oid SPNEGO_OID = getSpnegoOid(); @@ -86,77 +70,45 @@ private static Oid getSpnegoOid() { private final String userPrincipalName; private final SecureString password; private final String keytabPath; - private final Boolean enableDebugLogs; - private final Settings settings; + private final boolean enableDebugLogs; private LoginContext loginContext; /** - * Constructs {@link CustomHttpClientConfigCallbackHandler} with given + * Constructs {@link SpnegoHttpClientConfigCallbackHandler} with given * principalName and password. * * @param userPrincipalName user principal name * @param password password for user * @param enableDebugLogs if {@code true} enables kerberos debug logs - * @param settings {@link Settings} */ - public CustomHttpClientConfigCallbackHandler(final String userPrincipalName, final SecureString password, final Boolean enableDebugLogs, - final Settings settings) { + public SpnegoHttpClientConfigCallbackHandler(final String userPrincipalName, final SecureString password, final boolean enableDebugLogs) { this.userPrincipalName = userPrincipalName; this.password = password; this.keytabPath = null; this.enableDebugLogs = enableDebugLogs; - this.settings = settings; } /** - * Constructs {@link CustomHttpClientConfigCallbackHandler} with given + * Constructs {@link SpnegoHttpClientConfigCallbackHandler} with given * principalName and keytab. * * @param userPrincipalName User principal name * @param keytabPath path to keytab file for user + * @param enableDebugLogs if {@code true} enables kerberos debug logs */ - public CustomHttpClientConfigCallbackHandler(final String userPrincipalName, final String keytabPath, final Boolean enableDebugLogs, - final Settings settings) { + public SpnegoHttpClientConfigCallbackHandler(final String userPrincipalName, final String keytabPath, final boolean enableDebugLogs) { this.userPrincipalName = userPrincipalName; this.keytabPath = keytabPath; this.password = null; this.enableDebugLogs = enableDebugLogs; - this.settings = settings; } @Override public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { setupSpnegoAuthSchemeSupport(httpClientBuilder); - setupSSLSettings(httpClientBuilder); return httpClientBuilder; } - private void setupSSLSettings(HttpAsyncClientBuilder httpClientBuilder) { - final String keystorePath = settings.get(ESRestTestCase.TRUSTSTORE_PATH); - if (keystorePath != null) { - final String keystorePass = settings.get(ESRestTestCase.TRUSTSTORE_PASSWORD); - if (keystorePass == null) { - throw new IllegalStateException( - ESRestTestCase.TRUSTSTORE_PATH + " is provided but not " + ESRestTestCase.TRUSTSTORE_PASSWORD); - } - final Path path = PathUtils.get(keystorePath); - if (!Files.exists(path)) { - throw new IllegalStateException(ESRestTestCase.TRUSTSTORE_PATH + " is set but points to a non-existing file"); - } - try (InputStream is = Files.newInputStream(path)) { - final KeyStore keyStore = KeyStore.getInstance("jks"); - keyStore.load(is, keystorePass.toCharArray()); - final SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(keyStore, null).build(); - final SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(sslcontext); - httpClientBuilder.setSSLStrategy(sessionStrategy); - } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | CertificateException e) { - throw new RuntimeException("Error setting up ssl", e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - private void setupSpnegoAuthSchemeSupport(HttpAsyncClientBuilder httpClientBuilder) { final Lookup authSchemeRegistry = RegistryBuilder.create() .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory()).build(); @@ -165,9 +117,11 @@ private void setupSpnegoAuthSchemeSupport(HttpAsyncClientBuilder httpClientBuild try { final GSSName gssUserPrincipalName = gssManager.createName(userPrincipalName, GSSName.NT_USER_NAME); login(); - final GSSCredential credential = Subject.doAs(loginContext.getSubject(), + final AccessControlContext acc = AccessController.getContext(); + final GSSCredential credential = doAsPrivilegedWrapper(loginContext.getSubject(), (PrivilegedExceptionAction) () -> gssManager.createCredential(gssUserPrincipalName, - GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); + GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.INITIATE_ONLY), + acc); final KerberosCredentialsProvider credentialsProvider = new KerberosCredentialsProvider(); credentialsProvider.setCredentials( @@ -176,10 +130,8 @@ private void setupSpnegoAuthSchemeSupport(HttpAsyncClientBuilder httpClientBuild httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); } catch (GSSException e) { throw new RuntimeException(e); - } catch (LoginException e) { - throw new RuntimeException(e); } catch (PrivilegedActionException e) { - throw new RuntimeException(e); + throw new RuntimeException(e.getCause()); } httpClientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry); } @@ -189,27 +141,55 @@ private void setupSpnegoAuthSchemeSupport(HttpAsyncClientBuilder httpClientBuild * returns {@link LoginContext} * * @return {@link LoginContext} - * @throws LoginException + * @throws PrivilegedActionException */ - public LoginContext login() throws LoginException { + public synchronized LoginContext login() throws PrivilegedActionException { if (this.loginContext == null) { - final Subject subject = new Subject(false, Collections.singleton(new KerberosPrincipal(userPrincipalName)), - Collections.emptySet(), Collections.emptySet()); - Configuration conf = null; - final CallbackHandler callback; - if (password != null) { - conf = new PasswordJaasConf(userPrincipalName, enableDebugLogs); - callback = new KrbCallbackHandler(userPrincipalName, password); - } else { - conf = new KeytabJaasConf(userPrincipalName, keytabPath, enableDebugLogs); - callback = null; - } - loginContext = new LoginContext(CRED_CONF_NAME, subject, callback, conf); - loginContext.login(); + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + final Subject subject = new Subject(false, Collections.singleton(new KerberosPrincipal(userPrincipalName)), + Collections.emptySet(), Collections.emptySet()); + Configuration conf = null; + final CallbackHandler callback; + if (password != null) { + conf = new PasswordJaasConf(userPrincipalName, enableDebugLogs); + callback = new KrbCallbackHandler(userPrincipalName, password); + } else { + conf = new KeytabJaasConf(userPrincipalName, keytabPath, enableDebugLogs); + callback = null; + } + loginContext = new LoginContext(CRED_CONF_NAME, subject, callback, conf); + loginContext.login(); + return null; + }); } return loginContext; } + /** + * Privileged Wrapper that invokes action with Subject.doAs to perform work as + * given subject. + * + * @param subject {@link Subject} to be used for this work + * @param action {@link PrivilegedExceptionAction} action for performing inside + * Subject.doAs + * @param acc the {@link AccessControlContext} to be tied to the specified + * subject and action see + * {@link Subject#doAsPrivileged(Subject, PrivilegedExceptionAction, AccessControlContext) + * @return the value returned by the PrivilegedExceptionAction's run method + * @throws PrivilegedActionException + */ + static T doAsPrivilegedWrapper(final Subject subject, final PrivilegedExceptionAction action, final AccessControlContext acc) + throws PrivilegedActionException { + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAsPrivileged(subject, action, acc)); + } catch (PrivilegedActionException pae) { + if (pae.getCause() instanceof PrivilegedActionException) { + throw (PrivilegedActionException) pae.getCause(); + } + throw pae; + } + } + /** * This class matches {@link AuthScope} and based on that returns * {@link Credentials}. Only supports {@link AuthSchemes#SPNEGO} in @@ -276,7 +256,7 @@ public void handle(final Callback[] callbacks) throws IOException, UnsupportedCa */ private static class PasswordJaasConf extends AbstractJaasConf { - PasswordJaasConf(final String userPrincipalName, final Boolean enableDebugLogs) { + PasswordJaasConf(final String userPrincipalName, final boolean enableDebugLogs) { super(userPrincipalName, enableDebugLogs); } @@ -297,7 +277,7 @@ public void addOptions(final Map options) { private static class KeytabJaasConf extends AbstractJaasConf { private final String keytabFilePath; - KeytabJaasConf(final String userPrincipalName, final String keytabFilePath, final Boolean enableDebugLogs) { + KeytabJaasConf(final String userPrincipalName, final String keytabFilePath, final boolean enableDebugLogs) { super(userPrincipalName, enableDebugLogs); this.keytabFilePath = keytabFilePath; } @@ -312,9 +292,9 @@ public void addOptions(final Map options) { private abstract static class AbstractJaasConf extends Configuration { private final String userPrincipalName; - private final Boolean enableDebugLogs; + private final boolean enableDebugLogs; - AbstractJaasConf(final String userPrincipalName, final Boolean enableDebugLogs) { + AbstractJaasConf(final String userPrincipalName, final boolean enableDebugLogs) { this.userPrincipalName = userPrincipalName; this.enableDebugLogs = enableDebugLogs; } @@ -327,7 +307,7 @@ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { options.put("isInitiator", Boolean.TRUE.toString()); options.put("storeKey", Boolean.TRUE.toString()); options.put("renewTGT", Boolean.FALSE.toString()); - options.put("debug", enableDebugLogs.toString()); + options.put("debug", Boolean.toString(enableDebugLogs)); addOptions(options); return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Collections.unmodifiableMap(options)) }; diff --git a/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy new file mode 100644 index 0000000000000..82fb48a93dd81 --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy @@ -0,0 +1,5 @@ +grant { + permission javax.security.auth.AuthPermission "doAsPrivileged"; + permission java.io.FilePermission "${test.userkt.keytab}", "read"; + permission javax.security.auth.kerberos.DelegationPermission "\"HTTP/localhost@BUILD.ELASTIC.CO\" \"krbtgt/BUILD.ELASTIC.CO@BUILD.ELASTIC.CO\""; +}; \ No newline at end of file From 25a6b87304331693ec739530d669857993e56a28 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 17 Jul 2018 18:37:02 +1000 Subject: [PATCH 6/8] [Kerberos] Fix check style error. Forgot to run it. --- .../kerberos/SpnegoHttpClientConfigCallbackHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java index 4c87a8057a33e..a9a76b71c8535 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java @@ -76,12 +76,13 @@ private static Oid getSpnegoOid() { /** * Constructs {@link SpnegoHttpClientConfigCallbackHandler} with given * principalName and password. - * + * * @param userPrincipalName user principal name * @param password password for user * @param enableDebugLogs if {@code true} enables kerberos debug logs */ - public SpnegoHttpClientConfigCallbackHandler(final String userPrincipalName, final SecureString password, final boolean enableDebugLogs) { + public SpnegoHttpClientConfigCallbackHandler(final String userPrincipalName, final SecureString password, + final boolean enableDebugLogs) { this.userPrincipalName = userPrincipalName; this.password = password; this.keytabPath = null; From a92332880c142a449d54f627c11781455c774ffd Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 17 Jul 2018 20:19:15 +1000 Subject: [PATCH 7/8] [Kerberos] forbidden api check --- .../security/authc/kerberos/KerberosAuthenticationIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java index 189fb030177aa..d5928cb58f687 100644 --- a/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java @@ -85,7 +85,7 @@ public void setupRoleMapping() throws IOException { 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.getBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); + final boolean enabledDebugLogs = Boolean.parseBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_KEY)); final SpnegoHttpClientConfigCallbackHandler callbackHandler = new SpnegoHttpClientConfigCallbackHandler(userPrincipalName, keytabPath, enabledDebugLogs); executeRequestAndVerifyResponse(userPrincipalName, callbackHandler); @@ -94,7 +94,7 @@ public void testLoginByKeytab() throws IOException, PrivilegedActionException { 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.getBoolean(System.getProperty(ENABLE_KERBEROS_DEBUG_LOGS_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); From 74885402b3db812943e5d810e3888687772bbad6 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Wed, 18 Jul 2018 13:36:16 +1000 Subject: [PATCH 8/8] [Kerberos] Address review comment --- x-pack/qa/kerberos-tests/build.gradle | 14 ++++++++++++-- .../src/test/resources/plugin-security.policy | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index 025239f7a7463..e6d117fb17652 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -62,6 +62,13 @@ for (String principal : principals) { 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' @@ -96,11 +103,10 @@ integTestCluster { return tmpFile.exists() } - dependsOn krb5AddPrincipals, krb5kdcFixture } integTestRunner { - Path peppaKeytab = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("keytabs").resolve("peppa.keytab").toAbsolutePath() + 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}" @@ -113,4 +119,8 @@ integTestRunner { 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 } \ No newline at end of file diff --git a/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy index 82fb48a93dd81..fb7936bf62093 100644 --- a/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy +++ b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy @@ -1,5 +1,4 @@ grant { permission javax.security.auth.AuthPermission "doAsPrivileged"; - permission java.io.FilePermission "${test.userkt.keytab}", "read"; permission javax.security.auth.kerberos.DelegationPermission "\"HTTP/localhost@BUILD.ELASTIC.CO\" \"krbtgt/BUILD.ELASTIC.CO@BUILD.ELASTIC.CO\""; }; \ No newline at end of file