From 52d77016ca7bf9fbe58daf31dd4335dbf543d435 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Sun, 24 Jun 2018 01:30:33 +1000 Subject: [PATCH 1/9] Add support framework for Kerberos Realm (#31023) This change adds the framework to support Kerberos authN in elasticsearch. ES is the service protected by Kerberos, each ES service node will have its own keytab. Keytab is the file with Service principal name and encrypted key. This can be then used to validate the authenticator coming in the request. This change only adds support for SPNEGO mechanism and uses JGSS. JVM options -Djava.security.krb5.conf can be used to specify krb5.conf with additional settings if required. For Kerberos Realm, KerberosRealmSettings: Captures settings required for Kerberos Usually keytab (stored in the config), cache settings and krb debug flag KerberosAuthenticationToken: Handles extraction of token from request Extracts the token from request header: "Authorization: Negotiate " If any error condition occurs, throws Exception with Rest status 401 Also adds response header "WWW-Authenticate: Negotiate" KerberosTicketValidator: Used for kerberos ticket validation and gss context establishment. On service side, we need to login first, uses Jaas to complete service login. To avoid more file configurations, we generate the JAAS configuration with required modules in memory. The token extracted from authnToken is passed on to GSSContext which uses service credentials (keytab) to verify the passed token and generates output token. If GSS context is established it returns tuple of client-username and out token (can be empty). If out token is present but context is yet not established then it will return tuple with no username and out token. The out token needs to be returned as response header 401 and "WWW-Authenticate: Negotiate " for ongoing negotiation. This will continue till either it fails or successful authentication on context establishment. Changes in plugin-security policy to add required permissions Few settings like Jaas config and kerberos keytab access requires permissions. For testing, KerberosTestCase is the base class to start/stop kdc server and build test settings. SimpleKdcLdapServer is a wrapper around SimpleKdcServer(ApacheDS), which simplifies in memory testing with KDC and uses in-memory LDAP server as its backend. --- .../authc/kerberos/KerberosRealmSettings.java | 47 +++ x-pack/plugin/security/build.gradle | 63 +++- .../kerberos/KerberosAuthenticationToken.java | 117 ++++++++ .../support/KerberosTicketValidator.java | 278 ++++++++++++++++++ .../plugin-metadata/plugin-security.policy | 15 + .../KerberosAuthenticationTokenTests.java | 110 +++++++ .../kerberos/KerberosRealmSettingsTests.java | 43 +++ .../kerberos/support/KerberosTestCase.java | 218 ++++++++++++++ .../support/KerberosTicketValidatorTests.java | 101 +++++++ .../kerberos/support/SimpleKdcLdapServer.java | 224 ++++++++++++++ .../support/SimpleKdcLdapServerTests.java | 75 +++++ .../authc/kerberos/support/SpnegoClient.java | 258 ++++++++++++++++ .../security/src/test/resources/kdc.ldiff | 23 ++ 13 files changed, 1571 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationTokenTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java create mode 100644 x-pack/plugin/security/src/test/resources/kdc.ldiff diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java new file mode 100644 index 0000000000000..a3dfb501928ee --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java @@ -0,0 +1,47 @@ +/* + * 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.core.security.authc.kerberos; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.set.Sets; + +import java.util.Set; + +/** + * Kerberos Realm settings + */ +public final class KerberosRealmSettings { + public static final String TYPE = "kerberos"; + + /** + * Kerberos key tab for Elasticsearch service
+ * Uses single key tab for multiple service accounts. + */ + public static final Setting HTTP_SERVICE_KEYTAB_PATH = + Setting.simpleString("keytab.path", Property.NodeScope); + public static final Setting SETTING_KRB_DEBUG_ENABLE = + Setting.boolSetting("krb.debug", Boolean.FALSE, Property.NodeScope); + + // Cache + private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); + private static final int DEFAULT_MAX_USERS = 100_000; // 100k users + public static final Setting CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); + public static final Setting CACHE_MAX_USERS_SETTING = + Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Property.NodeScope); + + private KerberosRealmSettings() { + } + + /** + * @return the valid set of {@link Setting}s for a {@value #TYPE} realm + */ + public static Set> getSettings() { + return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE); + } +} diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 12533a389b5f1..c280a516ef823 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -52,12 +52,73 @@ dependencies { compile "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}" compile "org.apache.httpcomponents:httpclient-cache:${versions.httpclient}" - compile 'com.google.guava:guava:19.0' + compile 'com.google.guava:guava:19.0' testCompile 'org.elasticsearch:securemock:1.2' testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}" //testCompile "org.yaml:snakeyaml:${versions.snakeyaml}" + // Test dependencies for Kerberos (MiniKdc) + testCompile('commons-io:commons-io:2.5') + testCompile('org.apache.kerby:kerb-simplekdc:1.1.1') + testCompile('org.apache.kerby:kerb-client:1.1.1') + testCompile('org.apache.kerby:kerby-config:1.1.1') + testCompile('org.apache.kerby:kerb-core:1.1.1') + testCompile('org.apache.kerby:kerby-pkix:1.1.1') + testCompile('org.apache.kerby:kerby-asn1:1.1.1') + testCompile('org.apache.kerby:kerby-util:1.1.1') + testCompile('org.apache.kerby:kerb-common:1.1.1') + testCompile('org.apache.kerby:kerb-crypto:1.1.1') + testCompile('org.apache.kerby:kerb-util:1.1.1') + testCompile('org.apache.kerby:token-provider:1.1.1') + testCompile('com.nimbusds:nimbus-jose-jwt:4.41.2') + testCompile('net.jcip:jcip-annotations:1.0') + testCompile('org.apache.kerby:kerb-admin:1.1.1') + testCompile('org.apache.kerby:kerb-server:1.1.1') + testCompile('org.apache.kerby:kerb-identity:1.1.1') + testCompile('org.apache.kerby:kerby-xdr:1.1.1') + + // LDAP backend support for SimpleKdcServer + testCompile('org.apache.kerby:kerby-backend:1.1.1') + testCompile('org.apache.kerby:ldap-backend:1.1.1') + testCompile('org.apache.kerby:kerb-identity:1.1.1') + testCompile('org.apache.directory.api:api-ldap-client-api:1.0.0') + testCompile('org.apache.directory.api:api-ldap-schema-data:1.0.0') + testCompile('org.apache.directory.api:api-ldap-codec-core:1.0.0') + testCompile('org.apache.directory.api:api-ldap-extras-aci:1.0.0') + testCompile('org.apache.directory.api:api-ldap-extras-codec:1.0.0') + testCompile('org.apache.directory.api:api-ldap-extras-codec-api:1.0.0') + testCompile('commons-pool:commons-pool:1.6') + testCompile('commons-collections:commons-collections:3.2') + testCompile('org.apache.mina:mina-core:2.0.17') + testCompile('org.apache.directory.api:api-util:1.0.1') + testCompile('org.apache.directory.api:api-i18n:1.0.1') + testCompile('org.apache.directory.api:api-ldap-model:1.0.1') + testCompile('org.apache.directory.api:api-asn1-api:1.0.1') + testCompile('org.apache.directory.api:api-asn1-ber:1.0.1') + testCompile('org.apache.servicemix.bundles:org.apache.servicemix.bundles.antlr:2.7.7_5') + testCompile('org.apache.directory.server:apacheds-core-api:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-i18n:2.0.0-M24') + testCompile('org.apache.directory.api:api-ldap-extras-util:1.0.0') + testCompile('net.sf.ehcache:ehcache:2.10.4') + testCompile('org.apache.directory.server:apacheds-kerberos-codec:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-protocol-ldap:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-protocol-shared:2.0.0-M24') + testCompile('org.apache.directory.jdbm:apacheds-jdbm1:2.0.0-M3') + testCompile('org.apache.directory.server:apacheds-jdbm-partition:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-xdbm-partition:2.0.0-M24') + testCompile('org.apache.directory.api:api-ldap-extras-sp:1.0.0') + testCompile('org.apache.directory.server:apacheds-test-framework:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-core-annotations:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-ldif-partition:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-mavibot-partition:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-protocol-kerberos:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-server-annotations:2.0.0-M24') + testCompile('org.apache.directory.api:api-ldap-codec-standalone:1.0.0') + testCompile('org.apache.directory.api:api-ldap-net-mina:1.0.0') + testCompile('org.apache.directory.server:ldap-client-test:2.0.0-M24') + testCompile('org.apache.directory.server:apacheds-interceptor-kerberos:2.0.0-M24') + testCompile('org.apache.directory.mavibot:mavibot:1.0.0-M8') } compileJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes,-serial,-try,-unchecked" diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java new file mode 100644 index 0000000000000..e78624ee687d7 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java @@ -0,0 +1,117 @@ +/* + * 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.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; + +import java.util.Arrays; +import java.util.Base64; + +/** + * This class represents an AuthenticationToken for Kerberos authentication + * using SPNEGO. The token stores base 64 decoded token bytes, extracted from + * the Authorization header with auth scheme 'Negotiate'. + *

+ * Example Authorization header "Authorization: Negotiate + * YIIChgYGKwYBBQUCoII..." + *

+ * If there is any error handling during extraction of 'Negotiate' header then + * it throws {@link ElasticsearchSecurityException} with + * {@link RestStatus#UNAUTHORIZED} and header 'WWW-Authenticate: Negotiate' + */ +public final class KerberosAuthenticationToken implements AuthenticationToken { + + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + public static final String AUTH_HEADER = "Authorization"; + public static final String NEGOTIATE_SCHEME_NAME = "Negotiate"; + public static final String NEGOTIATE_AUTH_HEADER_PREFIX = NEGOTIATE_SCHEME_NAME + " "; + + // authorization scheme check is case-insensitive + private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; + + private final byte[] decodedToken; + + public KerberosAuthenticationToken(final byte[] decodedToken) { + this.decodedToken = decodedToken; + } + + /** + * Extract token from authorization header and if it is valid + * {@value #NEGOTIATE_AUTH_HEADER_PREFIX} then returns + * {@link KerberosAuthenticationToken} + * + * @param authorizationHeader Authorization header from request + * @return returns {@code null} if {@link #AUTH_HEADER} is empty or does not + * start with {@value #NEGOTIATE_AUTH_HEADER_PREFIX} else returns valid + * {@link KerberosAuthenticationToken} + * @throws ElasticsearchSecurityException when negotiate header is invalid. + */ + public static KerberosAuthenticationToken extractToken(final String authorizationHeader) { + if (Strings.isNullOrEmpty(authorizationHeader)) { + return null; + } + if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER_PREFIX, 0, + NEGOTIATE_AUTH_HEADER_PREFIX.length()) == false) { + return null; + } + + final String base64EncodedToken = authorizationHeader.substring(NEGOTIATE_AUTH_HEADER_PREFIX.length()).trim(); + if (Strings.isEmpty(base64EncodedToken)) { + throw unauthorized("invalid negotiate authentication header value, expected base64 encoded token but value is empty", null); + } + + byte[] decodedKerberosTicket = null; + try { + decodedKerberosTicket = Base64.getDecoder().decode(base64EncodedToken); + } catch (IllegalArgumentException iae) { + throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", iae, base64EncodedToken); + } + + return new KerberosAuthenticationToken(decodedKerberosTicket); + } + + @Override + public String principal() { + return ""; + } + + @Override + public Object credentials() { + return decodedToken; + } + + @Override + public void clearCredentials() { + Arrays.fill(decodedToken, (byte) 0); + } + + @Override + public int hashCode() { + return Arrays.hashCode(decodedToken); + } + + @Override + public boolean equals(final Object other) { + if (this == other) + return true; + if (other == null) + return false; + if (getClass() != other.getClass()) + return false; + final KerberosAuthenticationToken otherKerbToken = (KerberosAuthenticationToken) other; + return Arrays.equals(otherKerbToken.decodedToken, this.decodedToken); + } + + private static ElasticsearchSecurityException unauthorized(final String message, final Throwable cause, final Object... args) { + ElasticsearchSecurityException ese = new ElasticsearchSecurityException(message, RestStatus.UNAUTHORIZED, cause, args); + ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_SCHEME_NAME); + return ese; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java new file mode 100644 index 0000000000000..61e8f7500d5f5 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java @@ -0,0 +1,278 @@ +/* + * 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.support; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.ESLoggerFactory; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; + +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** + * Utility class that validates kerberos ticket for peer authentication. + *

+ * This class takes care of login by ES service credentials using keytab, + * GSSContext establishment, and then validating the incoming token. + *

+ * It may respond with token which needs to be communicated with the peer. + */ +public class KerberosTicketValidator { + 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 static final Logger LOGGER = ESLoggerFactory.getLogger(KerberosTicketValidator.class); + + private static final String KEY_TAB_CONF_NAME = "KeytabConf"; + private static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + + /** + * Validates client kerberos ticket received from the peer. + *

+ * First performs service login using keytab, supports multiple principals in + * keytab and the principal is selected based on the request. + *

+ * The GSS security context establishment is handled and if it is established then + * returns the Tuple of username and out token for peer reply. It may return + * Tuple with null username but with a outToken to be sent to peer for further + * negotiation. + * + * @param decodedToken base64 decoded kerberos ticket bytes + * @param keytabPath Path to Service key tab file containing credentials for ES + * service. + * @param krbDebug if {@code true} enables jaas krb5 login module debug logs. + * @return {@link Tuple} of user name {@link GSSContext#getSrcName()} and out + * token base64 encoded if any. When context is not yet established user + * name is {@code null}. + * @throws LoginException thrown when service authentication fails + * {@link LoginContext#login()} + * @throws GSSException thrown when GSS Context negotiation fails + * {@link GSSException} + */ + public Tuple validateTicket(final byte[] decodedToken, final Path keytabPath, final boolean krbDebug) + throws LoginException, GSSException { + final GSSManager gssManager = GSSManager.getInstance(); + GSSContext gssContext = null; + LoginContext loginContext = null; + try { + loginContext = serviceLogin(keytabPath.toString(), krbDebug); + GSSCredential serviceCreds = createCredentials(gssManager, loginContext.getSubject()); + gssContext = gssManager.createContext(serviceCreds); + final String base64OutToken = encodeToString(acceptSecContext(decodedToken, gssContext, loginContext.getSubject())); + LOGGER.trace("validateTicket isGSSContextEstablished = {}, username = {}, outToken = {}", gssContext.isEstablished(), + gssContext.getSrcName().toString(), base64OutToken); + return new Tuple<>(gssContext.isEstablished() ? gssContext.getSrcName().toString() : null, base64OutToken); + } catch (PrivilegedActionException pve) { + if (pve.getCause() instanceof LoginException) { + throw (LoginException) pve.getCause(); + } + if (pve.getCause() instanceof GSSException) { + throw (GSSException) pve.getCause(); + } + throw ExceptionsHelper.convertToRuntime(pve.getException()); + } finally { + privilegedLogoutNoThrow(loginContext); + privilegedDisposeNoThrow(gssContext); + } + } + + /** + * Encodes the specified byte array using base64 encoding scheme + * + * @param outToken byte array to be encoded + * @return String containing base64 encoded characters. returns {@code null} if + * outToken is null or empty. + */ + private String encodeToString(final byte[] outToken) { + if (outToken != null && outToken.length > 0) { + return Base64.getEncoder().encodeToString(outToken); + } + return null; + } + + /** + * Handles GSS context establishment. Received token is passed to the GSSContext + * on acceptor side and returns with out token that needs to be sent to peer for + * further GSS context establishment. + *

+ * + * @param base64decodedTicket in token generated by peer + * @param gssContext instance of acceptor {@link GSSContext} + * @param subject authenticated subject + * @return a byte[] containing the token to be sent to the peer. null indicates + * that no token is generated. + * @throws PrivilegedActionException + * @see GSSContext#acceptSecContext(byte[], int, int) + */ + private static byte[] acceptSecContext(final byte[] base64decodedTicket, final GSSContext gssContext, Subject subject) + throws PrivilegedActionException { + // process token with gss context + return doAsWrapper(subject, + (PrivilegedExceptionAction) () -> gssContext.acceptSecContext(base64decodedTicket, 0, base64decodedTicket.length)); + } + + /** + * For acquiring SPNEGO mechanism credentials for service based on the subject + * + * @param gssManager {@link GSSManager} + * @param subject logged in {@link Subject} + * @return {@link GSSCredential} for particular mechanism + * @throws GSSException + * @throws PrivilegedActionException + */ + private static GSSCredential createCredentials(final GSSManager gssManager, + final Subject subject) throws GSSException, PrivilegedActionException { + return doAsWrapper(subject, (PrivilegedExceptionAction) () -> gssManager + .createCredential(null, GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.ACCEPT_ONLY)); + } + + /** + * 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 + * @return the value returned by the PrivilegedExceptionAction's run method + * @throws PrivilegedActionException + */ + private static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction action) throws PrivilegedActionException { + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); + } catch (PrivilegedActionException pae) { + if (pae.getCause() instanceof PrivilegedActionException) { + throw (PrivilegedActionException) pae.getCause(); + } + throw pae; + } + } + + /** + * Privileged wrapper for closing GSSContext, does not throw exceptions but logs + * them as a debug message. + * + * @param gssContext GSSContext to be disposed. + */ + private static void privilegedDisposeNoThrow(final GSSContext gssContext) { + if (gssContext != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + gssContext.dispose(); + return null; + }); + } catch (PrivilegedActionException e) { + LOGGER.debug("Could not dispose GSS Context", e.getCause()); + } + } + } + + /** + * Privileged wrapper for closing LoginContext, does not throw exceptions but + * logs them as a debug message. + * + * @param loginContext LoginContext to be closed + */ + private static void privilegedLogoutNoThrow(final LoginContext loginContext) { + if (loginContext != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + loginContext.logout(); + return null; + }); + } catch (PrivilegedActionException e) { + LOGGER.debug("Could not close LoginContext", e.getCause()); + } + } + } + + /** + * Performs authentication using provided keytab + * + * @param keytabFilePath Keytab file path + * @param krbDebug if {@code true} enables jaas krb5 login module debug logs. + * @return authenticated {@link LoginContext} instance. Note: This needs to be + * closed using {@link LoginContext#logout()} after usage. + * @throws PrivilegedActionException when privileged action threw exception + */ + private static LoginContext serviceLogin(final String keytabFilePath, final boolean krbDebug) + throws PrivilegedActionException { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + final Subject subject = new Subject(false, Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + final Configuration conf = new KeytabJaasConf(keytabFilePath, krbDebug); + final LoginContext loginContext = new LoginContext(KEY_TAB_CONF_NAME, subject, null, conf); + loginContext.login(); + return loginContext; + }); + } + + /** + * 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 + * avoid 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. + */ + static class KeytabJaasConf extends Configuration { + private final String keytabFilePath; + private final boolean krbDebug; + + KeytabJaasConf(final String keytabFilePath, final boolean krbDebug) { + this.keytabFilePath = keytabFilePath; + this.krbDebug = krbDebug; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final Map options = new HashMap<>(); + options.put("keyTab", keytabFilePath); + /* + * As acceptor, we can have multiple SPNs, we do not want to use particular + * principal so it uses "*" + */ + options.put("principal", "*"); + options.put("useKeyTab", Boolean.TRUE.toString()); + options.put("storeKey", Boolean.TRUE.toString()); + options.put("doNotPrompt", Boolean.TRUE.toString()); + options.put("renewTGT", Boolean.FALSE.toString()); + options.put("refreshKrb5Config", Boolean.TRUE.toString()); + options.put("isInitiator", Boolean.FALSE.toString()); + options.put("debug", Boolean.toString(krbDebug)); + + return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Collections.unmodifiableMap(options)) }; + } + + } +} diff --git a/x-pack/plugin/security/src/main/plugin-metadata/plugin-security.policy b/x-pack/plugin/security/src/main/plugin-metadata/plugin-security.policy index 857c2f6e472d5..8ce72be3ef9a7 100644 --- a/x-pack/plugin/security/src/main/plugin-metadata/plugin-security.policy +++ b/x-pack/plugin/security/src/main/plugin-metadata/plugin-security.policy @@ -10,6 +10,21 @@ grant { // needed for multiple server implementations used in tests permission java.net.SocketPermission "*", "accept,connect"; + + // needed for Kerberos login + permission javax.security.auth.AuthPermission "modifyPrincipals"; + permission javax.security.auth.AuthPermission "modifyPrivateCredentials"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosKey * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KeyTab * \"*\"", "read"; + permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosTicket * \"*\"", "read"; + permission javax.security.auth.AuthPermission "doAs"; + permission javax.security.auth.kerberos.ServicePermission "*","initiate,accept"; + + permission java.util.PropertyPermission "javax.security.auth.useSubjectCredsOnly","write"; + permission java.util.PropertyPermission "java.security.krb5.conf","write"; + permission java.util.PropertyPermission "sun.security.krb5.debug","write"; + permission java.util.PropertyPermission "java.security.debug","write"; + permission java.util.PropertyPermission "sun.security.spnego.debug","write"; }; grant codeBase "${codebase.xmlsec-2.0.8.jar}" { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationTokenTests.java new file mode 100644 index 0000000000000..eaba796b41fe4 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationTokenTests.java @@ -0,0 +1,110 @@ +/* + * 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.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +public class KerberosAuthenticationTokenTests extends ESTestCase { + + private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; + + public void testExtractTokenForValidAuthorizationHeader() throws IOException { + final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); + final String negotiate = randomBoolean() ? KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX : "negotiate "; + final String authzHeader = negotiate + base64Token; + + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(authzHeader); + assertNotNull(kerbAuthnToken); + assertEquals(UNAUTHENTICATED_PRINCIPAL_NAME, kerbAuthnToken.principal()); + assertThat(kerbAuthnToken.credentials(), instanceOf((byte[].class))); + assertArrayEquals(Base64.getDecoder().decode(base64Token), (byte[]) kerbAuthnToken.credentials()); + } + + public void testExtractTokenForInvalidNegotiateAuthorizationHeaderShouldReturnNull() throws IOException { + final String header = randomFrom("negotiate", "Negotiate", " Negotiate", "NegotiateToken", "Basic ", " Custom ", null); + assertNull(KerberosAuthenticationToken.extractToken(header)); + } + + public void testExtractTokenForNegotiateAuthorizationHeaderWithNoTokenShouldThrowException() throws IOException { + final String header = randomFrom(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX, "negotiate ", "Negotiate "); + final ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> KerberosAuthenticationToken.extractToken(header)); + assertThat(e.getMessage(), + equalTo("invalid negotiate authentication header value, expected base64 encoded token but value is empty")); + assertContainsAuthenticateHeader(e); + } + + public void testExtractTokenForNotBase64EncodedTokenThrowsException() throws IOException { + final String notBase64Token = "[B@6499375d"; + + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + notBase64Token)); + assertThat(e.getMessage(), + equalTo("invalid negotiate authentication header value, could not decode base64 token " + notBase64Token)); + assertContainsAuthenticateHeader(e); + } + + public void testKerberoAuthenticationTokenClearCredentials() { + byte[] inputBytes = randomByteArrayOfLength(5); + final String base64Token = Base64.getEncoder().encodeToString(inputBytes); + final KerberosAuthenticationToken kerbAuthnToken = + KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + base64Token); + kerbAuthnToken.clearCredentials(); + Arrays.fill(inputBytes, (byte) 0); + assertArrayEquals(inputBytes, (byte[]) kerbAuthnToken.credentials()); + } + + public void testEqualsHashCode() { + final KerberosAuthenticationToken kerberosAuthenticationToken = + new KerberosAuthenticationToken("base64EncodedToken".getBytes(StandardCharsets.UTF_8)); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { + return new KerberosAuthenticationToken((byte[]) original.credentials()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { + byte[] originalCreds = (byte[]) original.credentials(); + return new KerberosAuthenticationToken(Arrays.copyOf(originalCreds, originalCreds.length)); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { + return new KerberosAuthenticationToken((byte[]) original.credentials()); + }, KerberosAuthenticationTokenTests::mutateTestItem); + } + + private static KerberosAuthenticationToken mutateTestItem(KerberosAuthenticationToken original) { + switch (randomIntBetween(0, 2)) { + case 0: + return new KerberosAuthenticationToken(randomByteArrayOfLength(10)); + case 1: + return new KerberosAuthenticationToken("base64EncodedToken".getBytes(StandardCharsets.UTF_16)); + case 2: + return new KerberosAuthenticationToken("[B@6499375d".getBytes(StandardCharsets.UTF_8)); + default: + throw new IllegalArgumentException("unknown option"); + } + } + + private static void assertContainsAuthenticateHeader(ElasticsearchSecurityException e) { + assertThat(e.status(), is(RestStatus.UNAUTHORIZED)); + assertThat(e.getHeaderKeys(), hasSize(1)); + assertThat(e.getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE), notNullValue()); + assertThat(e.getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE), contains(KerberosAuthenticationToken.NEGOTIATE_SCHEME_NAME)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java new file mode 100644 index 0000000000000..876ebdc574136 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -0,0 +1,43 @@ +/* + * 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.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class KerberosRealmSettingsTests extends ESTestCase { + + public void testKerberosRealmSettings() throws IOException { + final Path dir = createTempDir(); + Path configDir = dir.resolve("config"); + if (Files.exists(configDir) == false) { + configDir = Files.createDirectory(configDir); + } + final String keytabPathConfig = "config" + dir.getFileSystem().getSeparator() + "http.keytab"; + KerberosTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); + final Integer maxUsers = randomInt(); + final String cacheTTL = randomLongBetween(10L, 100L) + "m"; + final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, true); + + assertThat(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings), equalTo(keytabPathConfig)); + assertThat(KerberosRealmSettings.CACHE_TTL_SETTING.get(settings), + equalTo(TimeValue.parseTimeValue(cacheTTL, KerberosRealmSettings.CACHE_TTL_SETTING.getKey()))); + assertThat(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(settings), equalTo(maxUsers)); + assertThat(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings), is(true)); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java new file mode 100644 index 0000000000000..9a9453a8064bd --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java @@ -0,0 +1,218 @@ +/* + * 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.support; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.security.auth.Subject; + +/** + * Base Test class for Kerberos. + *

+ * Takes care of starting {@link SimpleKdcLdapServer} as Kdc server backed by + * Ldap Server. + *

+ * Also assists in building principal names, creation of principals and realm + * settings. + */ +public abstract class KerberosTestCase extends ESTestCase { + + protected Settings globalSettings; + protected Settings settings; + protected List serviceUserNames; + protected List clientUserNames; + protected Path workDir = null; + + protected SimpleKdcLdapServer simpleKdcLdapServer; + + private static Locale restoreLocale; + private static Set unsupportedLocaleLanguages; + static { + unsupportedLocaleLanguages = new HashSet<>(); + /* + * arabic and other languages have problem due to handling of GeneralizedTime in + * SimpleKdcServer For more look at : + * org.apache.kerby.asn1.type.Asn1GeneralizedTime#toBytes() + */ + unsupportedLocaleLanguages.add("ar"); + unsupportedLocaleLanguages.add("ja"); + unsupportedLocaleLanguages.add("th"); + unsupportedLocaleLanguages.add("hi"); + } + + @BeforeClass + public static void setupKerberos() throws Exception { + if (isLocaleUnsupported()) { + Logger logger = Loggers.getLogger(KerberosTestCase.class); + logger.warn("Attempting to run Kerberos test on {} locale, but that breaks SimpleKdcServer. Switching to English.", + Locale.getDefault()); + restoreLocale = Locale.getDefault(); + Locale.setDefault(Locale.ENGLISH); + } + } + + @AfterClass + public static void restoreLocale() throws Exception { + if (restoreLocale != null) { + Locale.setDefault(restoreLocale); + restoreLocale = null; + } + } + + private static boolean isLocaleUnsupported() { + return unsupportedLocaleLanguages.contains(Locale.getDefault().getLanguage()); + } + + @Before + public void startSimpleKdcLdapServer() throws Exception { + workDir = createTempDir(); + globalSettings = Settings.builder().put("path.home", workDir).build(); + + final Path kdcLdiff = getDataPath("/kdc.ldiff"); + simpleKdcLdapServer = new SimpleKdcLdapServer(workDir, "com", "example", kdcLdiff); + + // Create SPNs and UPNs + serviceUserNames = new ArrayList<>(); + Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { + serviceUserNames.add("HTTP/" + randomAlphaOfLength(8)); + }); + final Path ktabPathForService = createPrincipalKeyTab(workDir, serviceUserNames.toArray(new String[0])); + clientUserNames = new ArrayList<>(); + Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { + String clientUserName = "client-" + randomAlphaOfLength(8); + clientUserNames.add(clientUserName); + try { + createPrincipal(clientUserName, "pwd".toCharArray()); + } catch (Exception e) { + throw ExceptionsHelper.convertToRuntime(e); + } + }); + settings = buildKerberosRealmSettings(ktabPathForService.toString()); + } + + @After + public void tearDownMiniKdc() throws IOException, PrivilegedActionException { + simpleKdcLdapServer.stop(); + } + + /** + * Creates principals and exports them to the keytab created in the directory. + * + * @param dir Directory where the key tab would be created. + * @param princNames principal names to be created + * @return {@link Path} to key tab file. + * @throws Exception + */ + protected Path createPrincipalKeyTab(final Path dir, final String... princNames) throws Exception { + final Path path = dir.resolve(randomAlphaOfLength(10) + ".keytab"); + simpleKdcLdapServer.createPrincipal(path, princNames); + return path; + } + + /** + * Creates principal with given name and password. + * + * @param principalName Principal name + * @param password Password + * @throws Exception + */ + protected void createPrincipal(final String principalName, final char[] password) throws Exception { + simpleKdcLdapServer.createPrincipal(principalName, new String(password)); + } + + /** + * Appends realm name to user to form principal name + * + * @param user user name + * @return principal name in the form user@REALM + */ + protected String principalName(final String user) { + return user + "@" + simpleKdcLdapServer.getRealm(); + } + + /** + * Invokes Subject.doAs inside a doPrivileged block + * + * @param subject {@link Subject} + * @param action {@link PrivilegedExceptionAction} action for performing inside + * Subject.doAs + * @return Type of value as returned by PrivilegedAction + * @throws PrivilegedActionException + */ + static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction action) throws PrivilegedActionException { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); + } + + /** + * Write content to provided keytab file. + * + * @param keytabPath {@link Path} to keytab file. + * @param content Content for keytab + * @return key tab path + * @throws IOException + */ + public static Path writeKeyTab(final Path keytabPath, final String content) throws IOException { + try (BufferedWriter bufferedWriter = Files.newBufferedWriter(keytabPath, StandardCharsets.US_ASCII)) { + bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); + } + return keytabPath; + } + + /** + * Build kerberos realm settings with default config and given keytab + * + * @param keytabPath key tab file path + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath) { + return buildKerberosRealmSettings(keytabPath, 100, "10m", true); + } + + /** + * Build kerberos realm settings + * + * @param keytabPath key tab file path + * @param maxUsersInCache max users to be maintained in cache + * @param cacheTTL time to live for cached entries + * @param enableDebugging for krb5 logs + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, + final boolean enableDebugging) { + final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) + .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) + .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) + .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging); + return builder.build(); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java new file mode 100644 index 0000000000000..602bb1265fe11 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java @@ -0,0 +1,101 @@ +/* + * 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.support; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.ietf.jgss.GSSException; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.PrivilegedActionException; +import java.util.Base64; + +import javax.security.auth.login.LoginException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class KerberosTicketValidatorTests extends KerberosTestCase { + + private KerberosTicketValidator kerberosTicketValidator = new KerberosTicketValidator(); + + public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Exception { + createPrincipalKeyTab(workDir, "differentServer"); + + // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); + try (SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer"));) { + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); + assertThat(base64KerbToken, is(notNullValue())); + + final Environment env = TestEnvironment.newEnvironment(globalSettings); + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + final GSSException gssException = expectThrows(GSSException.class, + () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); + } + } + + public void testInvalidKerbTicketFailsValidation() throws Exception { + final String base64KerbToken = Base64.getEncoder().encodeToString(randomByteArrayOfLength(5)); + + final Environment env = TestEnvironment.newEnvironment(globalSettings); + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + final GSSException gssException = expectThrows(GSSException.class, + () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + assertThat(gssException.getMajor(), equalTo(GSSException.DEFECTIVE_TOKEN)); + } + + public void testWhenKeyTabWithInvalidContentFailsValidation() + throws LoginException, GSSException, IOException, PrivilegedActionException { + // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); + try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames)));) { + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); + assertThat(base64KerbToken, is(notNullValue())); + + final Path ktabPath = writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); + settings = buildKerberosRealmSettings(ktabPath.toString()); + final Environment env = TestEnvironment.newEnvironment(globalSettings); + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + final GSSException gssException = expectThrows(GSSException.class, + () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); + } + } + + public void testValidKebrerosTicket() throws PrivilegedActionException, GSSException, LoginException { + // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); + try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames)));) { + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); + assertThat(base64KerbToken, is(notNullValue())); + + final Environment env = TestEnvironment.newEnvironment(globalSettings); + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + final Tuple userNameOutToken = + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true); + assertThat(userNameOutToken, is(notNullValue())); + assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); + assertThat(userNameOutToken.v2(), is(notNullValue())); + + final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + assertThat(outToken, is(nullValue())); + assertThat(spnegoClient.isEstablished(), is(true)); + } + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java new file mode 100644 index 0000000000000..02bc236b8ec3d --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java @@ -0,0 +1,224 @@ +/* + * 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.support; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; + +import org.apache.kerby.kerberos.kerb.KrbException; +import org.apache.kerby.kerberos.kerb.client.KrbConfig; +import org.apache.kerby.kerberos.kerb.server.KdcConfigKey; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.apache.kerby.util.NetworkUtil; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Utility wrapper around Apache {@link SimpleKdcServer} backed by Unboundid + * {@link InMemoryDirectoryServer}.
+ * Starts in memory Ldap server and then uses it as backend for Kdc Server. + */ +public class SimpleKdcLdapServer { + private static final Logger logger = Loggers.getLogger(SimpleKdcLdapServer.class); + + private Path workDir = null; + private SimpleKdcServer simpleKdc; + private InMemoryDirectoryServer ldapServer; + + // KDC properties + private String transport = ESTestCase.randomFrom("TCP", "UDP"); + private int kdcPort = 0; + private String host; + private String realm; + private boolean krb5DebugBackupConfigValue; + + // LDAP properties + private String baseDn; + private Path ldiff; + private int ldapPort; + + /** + * Constructor for SimpleKdcLdapServer, creates instance of Kdc server and ldap + * backend server. Also initializes and starts them with provided configuration. + *

+ * To stop the KDC and ldap server use {@link #stop()} + * + * @param workDir Base directory for server, used to locate kdc.conf, + * backend.conf and kdc.ldiff + * @param orgName Org name for base dn + * @param domainName domain name for base dn + * @param ldiff for ldap directory. + * @throws Exception + */ + public SimpleKdcLdapServer(final Path workDir, final String orgName, final String domainName, final Path ldiff) throws Exception { + this.workDir = workDir; + this.realm = domainName.toUpperCase(Locale.ROOT) + "." + orgName.toUpperCase(Locale.ROOT); + this.baseDn = "dc=" + domainName + ",dc=" + orgName; + this.ldiff = ldiff; + this.krb5DebugBackupConfigValue = AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + @SuppressForbidden(reason = "set or clear system property krb5 debug in kerberos tests") + public Boolean run() throws Exception { + boolean oldDebugSetting = Boolean.parseBoolean(System.getProperty("sun.security.krb5.debug")); + System.setProperty("sun.security.krb5.debug", Boolean.TRUE.toString()); + return oldDebugSetting; + } + }); + + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + init(); + return null; + } + }); + logger.info("SimpleKdcLdapServer started."); + } + + @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File in order to create a SimpleKdcServer") + private void init() throws Exception { + // start ldap server + createLdapServiceAndStart(); + // create ldap backend conf + createLdapBackendConf(); + // Kdc Server + simpleKdc = new SimpleKdcServer(this.workDir.toFile(), new KrbConfig()); + prepareKdcServerAndStart(); + } + + private void createLdapServiceAndStart() throws Exception { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); + config.setSchema(null); + ldapServer = new InMemoryDirectoryServer(config); + ldapServer.importFromLDIF(true, this.ldiff.toString()); + ldapServer.startListening(); + ldapPort = ldapServer.getListenPort(); + } + + private void createLdapBackendConf() throws IOException { + String backendConf = KdcConfigKey.KDC_IDENTITY_BACKEND.getPropertyKey() + + " = org.apache.kerby.kerberos.kdc.identitybackend.LdapIdentityBackend\n" + "host=127.0.0.1\n" + "port=" + ldapPort + "\n" + + "admin_dn=uid=admin,ou=system," + baseDn + "\n" + "admin_pw=secret\n" + "base_dn=" + baseDn; + Files.write(this.workDir.resolve("backend.conf"), backendConf.getBytes(StandardCharsets.UTF_8)); + assert Files.exists(this.workDir.resolve("backend.conf")); + } + + @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File in order to create a SimpleKdcServer") + private void prepareKdcServerAndStart() throws Exception { + // transport + simpleKdc.setWorkDir(workDir.toFile()); + simpleKdc.setKdcHost(host); + simpleKdc.setKdcRealm(realm); + if (kdcPort == 0) { + kdcPort = NetworkUtil.getServerPort(); + } + if (transport != null) { + if (transport.trim().equals("TCP")) { + simpleKdc.setKdcTcpPort(kdcPort); + simpleKdc.setAllowUdp(false); + } else if (transport.trim().equals("UDP")) { + simpleKdc.setKdcUdpPort(kdcPort); + simpleKdc.setAllowTcp(false); + } else { + throw new IllegalArgumentException("Invalid transport: " + transport); + } + } else { + throw new IllegalArgumentException("Need to set transport!"); + } + final TimeValue minimumTicketLifeTime = new TimeValue(1, TimeUnit.DAYS); + final TimeValue maxRenewableLifeTime = new TimeValue(7, TimeUnit.DAYS); + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, minimumTicketLifeTime.getMillis()); + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, maxRenewableLifeTime.getMillis()); + simpleKdc.init(); + simpleKdc.start(); + } + + public String getRealm() { + return realm; + } + + public int getLdapListenPort() { + return ldapPort; + } + + public int getKdcPort() { + return kdcPort; + } + + /** + * Creates a principal in the KDC with the specified user and password. + * + * @param principal principal name, do not include the domain. + * @param password password. + * @throws Exception thrown if the principal could not be created. + */ + public synchronized void createPrincipal(final String principal, final String password) throws Exception { + simpleKdc.createPrincipal(principal, password); + } + + /** + * Creates multiple principals in the KDC and adds them to a keytab file. + * + * @param keytabFile keytab file to add the created principals. If keytab file + * exists and then always appends to it. + * @param principals principals to add to the KDC, do not include the domain. + * @throws Exception thrown if the principals or the keytab file could not be + * created. + */ + @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File in order to create a SimpleKdcServer") + public synchronized void createPrincipal(final Path keytabFile, final String... principals) throws Exception { + simpleKdc.createPrincipals(principals); + for (String principal : principals) { + simpleKdc.getKadmin().exportKeytab(keytabFile.toFile(), principal); + } + } + + /** + * Stop Simple Kdc Server + * + * @throws PrivilegedActionException + */ + public synchronized void stop() throws PrivilegedActionException { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + @SuppressForbidden(reason = "set or clear system property krb5 debug in kerberos tests") + public Void run() throws Exception { + if (simpleKdc != null) { + try { + simpleKdc.stop(); + } catch (KrbException e) { + throw ExceptionsHelper.convertToRuntime(e); + } finally { + System.setProperty("sun.security.krb5.debug", Boolean.toString(krb5DebugBackupConfigValue)); + } + } + + if (ldapServer != null) { + ldapServer.shutDown(true); + } + return null; + } + }); + logger.info("SimpleKdcServer stoppped."); + } + +} \ No newline at end of file diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java new file mode 100644 index 0000000000000..c82d4e9502a4f --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java @@ -0,0 +1,75 @@ +/* + * 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.support; + +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchScope; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils; +import org.ietf.jgss.GSSException; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivilegedActionException; +import java.text.ParseException; +import java.util.Base64; + +import javax.security.auth.login.LoginException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.notNullValue; + +public class SimpleKdcLdapServerTests extends KerberosTestCase { + + public void testPrincipalCreationAndSearchOnLdap() throws Exception { + simpleKdcLdapServer.createPrincipal(workDir.resolve("p1p2.keytab"), "p1", "p2"); + assertTrue(Files.exists(workDir.resolve("p1p2.keytab"))); + try (LDAPConnection ldapConn = + LdapUtils.privilegedConnect(() -> new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()));) { + assertThat(ldapConn.isConnected(), is(true)); + SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(krb5PrincipalName=p1@EXAMPLE.COM)"); + assertThat(sr.getSearchEntries(), hasSize(1)); + assertThat(sr.getSearchEntries().get(0).getDN(), equalTo("uid=p1,dc=example,dc=com")); + } + } + + public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException, ParseException { + final String serviceUserName = randomFrom(serviceUserNames); + // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); + try (SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName));) { + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); + assertThat(base64KerbToken, is(notNullValue())); + final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); + + // Service Login + final Environment env = TestEnvironment.newEnvironment(globalSettings); + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + // Handle Authz header which contains base64 token + final Tuple userNameOutToken = + new KerberosTicketValidator().validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, true); + assertThat(userNameOutToken, is(notNullValue())); + assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); + + // Authenticate service on client side. + final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + assertThat(outToken, is(nullValue())); + assertThat(spnegoClient.isEstablished(), is(true)); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java new file mode 100644 index 0000000000000..527953f8b2d46 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java @@ -0,0 +1,258 @@ +/* + * 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.support; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.logging.ESLoggerFactory; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; + +import java.io.IOException; +import java.security.AccessController; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +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 is used as a Spnego client during testing and handles SPNEGO + * interactions using GSS context negotiation.
+ * It is not advisable to share a SpnegoClient between threads as there is no + * synchronization in place, internally this depends on {@link GSSContext} for + * context negotiation which maintains sequencing for replay detections.
+ * Use {@link #close()} to release and dispose {@link LoginContext} and + * {@link GSSContext} after usage. + */ +class SpnegoClient implements AutoCloseable { + private static final Logger LOGGER = ESLoggerFactory.getLogger(SpnegoClient.class); + + public static final String CRED_CONF_NAME = "PasswordConf"; + private static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; + private final GSSManager gssManager = GSSManager.getInstance(); + private final LoginContext loginContext; + private final GSSContext gssContext; + + /** + * Creates SpengoClient to interact with given service principal
+ * Use {@link #close()} to logout {@link LoginContext} and dispose + * {@link GSSContext} after usage. + * @param userPrincipalName User principal name for login as client + * @param password password for client + * @param servicePrincipalName Service principal name with whom this client + * interacts with. + * @throws PrivilegedActionException + * @throws GSSException + */ + SpnegoClient(final String userPrincipalName, final SecureString password, final String servicePrincipalName) + throws PrivilegedActionException, GSSException { + String oldUseSubjectCredsOnlyFlag = null; + try { + oldUseSubjectCredsOnlyFlag = getAndSetUseSubjectCredsOnlySystemProperty("true"); + LOGGER.info("SpnegoClient with userPrincipalName : {}", userPrincipalName); + final GSSName gssUserPrincipalName = gssManager.createName(userPrincipalName, GSSName.NT_USER_NAME); + final GSSName gssServicePrincipalName = gssManager.createName(servicePrincipalName, GSSName.NT_USER_NAME); + loginContext = AccessController + .doPrivileged((PrivilegedExceptionAction) () -> loginUsingPassword(userPrincipalName, password)); + final GSSCredential userCreds = KerberosTestCase.doAsWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssManager.createCredential(gssUserPrincipalName, + GSSCredential.DEFAULT_LIFETIME, KerberosTicketValidator.SPNEGO_OID, GSSCredential.INITIATE_ONLY)); + gssContext = gssManager.createContext(gssServicePrincipalName.canonicalize(KerberosTicketValidator.SPNEGO_OID), + KerberosTicketValidator.SPNEGO_OID, userCreds, GSSCredential.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + } catch (PrivilegedActionException pve) { + LOGGER.error("privileged action exception, with root cause", pve.getException()); + throw pve; + } finally { + getAndSetUseSubjectCredsOnlySystemProperty(oldUseSubjectCredsOnlyFlag); + } + } + + /** + * GSSContext initiator side handling, initiates context establishment and returns the + * base64 encoded token to be sent to server. + * + * @return Base64 encoded token + * @throws PrivilegedActionException + */ + String getBase64EncodedTokenForSpnegoHeader() throws PrivilegedActionException { + final byte[] outToken = KerberosTestCase.doAsWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssContext.initSecContext(new byte[0], 0, 0)); + return Base64.getEncoder().encodeToString(outToken); + } + + /** + * Handles server response and returns new token if any to be sent to server. + * + * @param base64Token inToken received from server passed to initSecContext for + * gss negotiation + * @return Base64 encoded token to be sent to server. May return {@code null} if + * nothing to be sent. + * @throws PrivilegedActionException + */ + String handleResponse(final String base64Token) throws PrivilegedActionException { + if (gssContext.isEstablished()) { + throw new IllegalStateException("GSS Context has already been established"); + } + final byte[] token = Base64.getDecoder().decode(base64Token); + final byte[] outToken = KerberosTestCase.doAsWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssContext.initSecContext(token, 0, token.length)); + if (outToken == null || outToken.length == 0) { + return null; + } + return Base64.getEncoder().encodeToString(outToken); + } + + /** + * Spnego Client after usage needs to be closed in order to logout from + * {@link LoginContext} and dispose {@link GSSContext} + */ + public void close() throws LoginException, GSSException, PrivilegedActionException { + if (loginContext != null) { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + loginContext.logout(); + return null; + }); + } + if (gssContext != null) { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + gssContext.dispose(); + return null; + }); + } + } + + /** + * @return {@code true} If the context was established + */ + boolean isEstablished() { + return gssContext.isEstablished(); + } + + /** + * Performs authentication using provided principal name and password for client + * + * @param principal Principal name + * @param password {@link SecureString} + * @param settings {@link Settings} + * @return authenticated {@link LoginContext} instance. Note: This needs to be + * closed {@link LoginContext#logout()} after usage. + * @throws LoginException + */ + private static LoginContext loginUsingPassword(final String principal, final SecureString password) throws LoginException { + final Set principals = Collections.singleton(new KerberosPrincipal(principal)); + + final Subject subject = new Subject(false, principals, Collections.emptySet(), Collections.emptySet()); + + final Configuration conf = new PasswordJaasConf(principal); + final CallbackHandler callback = new KrbCallbackHandler(principal, password); + final LoginContext loginContext = new LoginContext(CRED_CONF_NAME, subject, callback, conf); + loginContext.login(); + return loginContext; + } + + /** + * 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 uing this instead of jaas.conf, this requires refresh of + * {@link Configuration} and reqires appropriate security permissions to do so. + */ + static class PasswordJaasConf extends Configuration { + private final String principal; + + PasswordJaasConf(final String principal) { + this.principal = principal; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final Map options = new HashMap<>(); + options.put("principal", principal); + options.put("storeKey", Boolean.TRUE.toString()); + options.put("useTicketCache", Boolean.FALSE.toString()); + options.put("useKeyTab", Boolean.FALSE.toString()); + options.put("renewTGT", Boolean.FALSE.toString()); + options.put("refreshKrb5Config", Boolean.TRUE.toString()); + options.put("isInitiator", Boolean.TRUE.toString()); + options.put("debug", Boolean.TRUE.toString()); + + return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Collections.unmodifiableMap(options)) }; + } + } + + /** + * Jaas call back handler to provide credentials. + */ + 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; + } + } + } + } + } + + private static String getAndSetUseSubjectCredsOnlySystemProperty(final String value) { + String retVal = null; + try { + retVal = AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + @SuppressForbidden( + reason = "For tests where we provide credentials, need to set and reset javax.security.auth.useSubjectCredsOnly") + public String run() throws Exception { + String oldValue = System.getProperty("javax.security.auth.useSubjectCredsOnly"); + if (value != null) { + System.setProperty("javax.security.auth.useSubjectCredsOnly", value); + } + return oldValue; + } + + }); + } catch (PrivilegedActionException e) { + throw ExceptionsHelper.convertToRuntime(e); + } + return retVal; + } +} diff --git a/x-pack/plugin/security/src/test/resources/kdc.ldiff b/x-pack/plugin/security/src/test/resources/kdc.ldiff new file mode 100644 index 0000000000000..e213048d6f578 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/kdc.ldiff @@ -0,0 +1,23 @@ +dn: dc=example,dc=com +objectClass: top +objectClass: domain +dc: example + +dn: ou=system,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: system + +dn: ou=users,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=admin,ou=system,dc=example,dc=com +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Admin +sn: Admin +uid: admin +userPassword: secret \ No newline at end of file From 88999208e7237d3d3e08d0dbd794d7db083272d9 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Wed, 27 Jun 2018 10:18:32 +1000 Subject: [PATCH 2/9] [Kerberos] Add bootstrap checks for kerberos realm (#31548) As there are some system properties like `java.security.krb5.kdc` , `java.security.krb5.realm` which can specify values that are applicable to whole JVM. This is the reason for having only one instance of Kerberos realm. Each ES node will have a Kerberos keytab with credentials. This keytab must exist for Kerberos authentication to work. `KerberosRealmBootstrapCheck` performs these checks for given configuration. --- .../xpack/security/Security.java | 4 +- .../kerberos/KerberosRealmBootstrapCheck.java | 69 +++++++++++ .../KerberosRealmBootstrapCheckTests.java | 115 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheck.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index dbb50a92f1088..88d9e1dfc9d13 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -167,6 +167,7 @@ import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealmBootstrapCheck; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener; @@ -285,7 +286,8 @@ public Security(Settings settings, final Path configPath) { checks.addAll(Arrays.asList( new TokenSSLBootstrapCheck(), new PkiRealmBootstrapCheck(settings, getSslService()), - new TLSLicenseBootstrapCheck())); + new TLSLicenseBootstrapCheck(), + new KerberosRealmBootstrapCheck(env))); checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); this.bootstrapChecks = Collections.unmodifiableList(checks); } else { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheck.java new file mode 100644 index 0000000000000..bab899a866425 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheck.java @@ -0,0 +1,69 @@ +/* + * 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.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Map.Entry; + +/** + * This class is used to perform bootstrap checks for kerberos realm. + *

+ * We use service keytabs for validating incoming kerberos tickets and is a + * required configuration. Due to JVM wide system properties for Kerberos we + * cannot support multiple Kerberos realms. This class adds checks for node to + * fail if service keytab does not exist or multiple kerberos realms have been + * configured. + */ +public class KerberosRealmBootstrapCheck implements BootstrapCheck { + private final Environment env; + + public KerberosRealmBootstrapCheck(final Environment env) { + this.env = env; + } + + @Override + public BootstrapCheckResult check(final BootstrapContext context) { + final Map realmsSettings = RealmSettings.getRealmSettings(context.settings); + boolean isKerberosRealmConfigured = false; + for (final Entry entry : realmsSettings.entrySet()) { + final String name = entry.getKey(); + final Settings realmSettings = entry.getValue(); + final String type = realmSettings.get("type"); + if (Strings.hasText(type) == false) { + return BootstrapCheckResult.failure("missing realm type for [" + name + "] realm"); + } + if (KerberosRealmSettings.TYPE.equals(type)) { + if (isKerberosRealmConfigured) { + return BootstrapCheckResult.failure( + "multiple [" + type + "] realms are configured. [" + type + "] can only have one such realm configured"); + } + isKerberosRealmConfigured = true; + + final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(realmSettings)); + if (Files.exists(keytabPath) == false) { + return BootstrapCheckResult.failure("configured service key tab file [" + keytabPath + "] does not exist"); + } + } + } + return BootstrapCheckResult.success(); + } + + @Override + public boolean alwaysEnforce() { + return true; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java new file mode 100644 index 0000000000000..d2a40f0f6162f --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java @@ -0,0 +1,115 @@ +/* + * 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.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class KerberosRealmBootstrapCheckTests extends ESTestCase { + + public void testBootstrapCheckFailsForMultipleKerberosRealms() throws IOException { + final Path tempDir = createTempDir(); + final Settings settings1 = buildKerberosRealmSettings("kerb1", false, tempDir); + final Settings settings2 = buildKerberosRealmSettings("kerb2", false, tempDir); + final Settings settings3 = realm("pki1", PkiRealmSettings.TYPE, Settings.builder()).build(); + final Settings settings = + Settings.builder().put("path.home", tempDir).put(settings1).put(settings2).put(settings3).build(); + final BootstrapContext context = new BootstrapContext(settings, null); + final KerberosRealmBootstrapCheck kerbRealmBootstrapCheck = + new KerberosRealmBootstrapCheck(TestEnvironment.newEnvironment(settings)); + final BootstrapCheck.BootstrapCheckResult result = kerbRealmBootstrapCheck.check(context); + assertThat(result, is(notNullValue())); + assertThat(result.isFailure(), is(true)); + assertThat(result.getMessage(), equalTo("multiple [" + KerberosRealmSettings.TYPE + "] realms are configured. [" + + KerberosRealmSettings.TYPE + "] can only have one such realm configured")); + } + + public void testBootstrapCheckFailsForMissingKeytabFile() throws IOException { + final Path tempDir = createTempDir(); + final Settings settings = + Settings.builder().put("path.home", tempDir).put(buildKerberosRealmSettings("kerb1", true, tempDir)).build(); + final BootstrapContext context = new BootstrapContext(settings, null); + final KerberosRealmBootstrapCheck kerbRealmBootstrapCheck = + new KerberosRealmBootstrapCheck(TestEnvironment.newEnvironment(settings)); + final BootstrapCheck.BootstrapCheckResult result = kerbRealmBootstrapCheck.check(context); + assertThat(result, is(notNullValue())); + assertThat(result.isFailure(), is(true)); + assertThat(result.getMessage(), + equalTo("configured service key tab file [" + tempDir.resolve("kerb1.keytab").toString() + "] does not exist")); + } + + public void testBootstrapCheckFailsForMissingRealmType() throws IOException { + final Path tempDir = createTempDir(); + final String name = "kerb1"; + final Settings settings1 = buildKerberosRealmSettings("kerb1", false, tempDir); + final Settings settings2 = realm(name, randomFrom("", " "), Settings.builder()).build(); + final Settings settings = + Settings.builder().put("path.home", tempDir).put(settings1).put(settings2).build(); + final BootstrapContext context = new BootstrapContext(settings, null); + final KerberosRealmBootstrapCheck kerbRealmBootstrapCheck = + new KerberosRealmBootstrapCheck(TestEnvironment.newEnvironment(settings)); + final BootstrapCheck.BootstrapCheckResult result = kerbRealmBootstrapCheck.check(context); + assertThat(result, is(notNullValue())); + assertThat(result.isFailure(), is(true)); + assertThat(result.getMessage(), equalTo("missing realm type for [" + name + "] realm")); + } + + public void testBootstrapCheckSucceedsForCorrectConfiguration() throws IOException { + final Path tempDir = createTempDir(); + final Settings finalSettings = + Settings.builder().put("path.home", tempDir).put(buildKerberosRealmSettings("kerb1", false, tempDir)).build(); + final BootstrapContext context = new BootstrapContext(finalSettings, null); + final KerberosRealmBootstrapCheck kerbRealmBootstrapCheck = + new KerberosRealmBootstrapCheck(TestEnvironment.newEnvironment(finalSettings)); + final BootstrapCheck.BootstrapCheckResult result = kerbRealmBootstrapCheck.check(context); + assertThat(result, is(notNullValue())); + assertThat(result.isSuccess(), is(true)); + } + + public void testBootstrapCheckSucceedsForNoKerberosRealms() throws IOException { + final Path tempDir = createTempDir(); + final Settings finalSettings = Settings.builder().put("path.home", tempDir).build(); + final BootstrapContext context = new BootstrapContext(finalSettings, null); + final KerberosRealmBootstrapCheck kerbRealmBootstrapCheck = + new KerberosRealmBootstrapCheck(TestEnvironment.newEnvironment(finalSettings)); + final BootstrapCheck.BootstrapCheckResult result = kerbRealmBootstrapCheck.check(context); + assertThat(result, is(notNullValue())); + assertThat(result.isSuccess(), is(true)); + } + + private Settings buildKerberosRealmSettings(final String name, final boolean missingKeytab, final Path tempDir) throws IOException { + final Settings.Builder builder = Settings.builder(); + if (missingKeytab == false) { + KerberosTestCase.writeKeyTab(tempDir.resolve(name + ".keytab"), null); + } + builder.put(KerberosTestCase.buildKerberosRealmSettings(tempDir.resolve(name + ".keytab").toString())); + return realm(name, KerberosRealmSettings.TYPE, builder).build(); + } + + private Settings.Builder realm(final String name, final String type, final Settings.Builder settings) { + final String prefix = RealmSettings.PREFIX + name + "."; + if (type != null) { + settings.put("type", type); + } + final Settings.Builder builder = Settings.builder().put(settings.normalizePrefix(prefix).build(), false); + return builder; + } +} From 52367f271e6522b2e5ea582ed618741ac2431b22 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Tue, 3 Jul 2018 10:55:28 +1000 Subject: [PATCH 3/9] [Kerberos] Add support for list of auth challenge (#31594) Till now we had support for 'Basic', 'Bearer' auth schemes and this was sufficient for us to reply `WWW-Authenticate` header with one value either for `Basic` or `Bearer` for unauthorized access. After introducing Kerberos we will be supporting `Negotiate` scheme. As per [RFC7235](https://tools.ietf.org/html/rfc7235#section-4.1), we may respond with the list of challenges. This list is of auth schemes supported by the server. We can also have custom Realms defining their own response header value for 'WWW-Authenticate' header. This commit introduces a `getWWWAuthenticateHeaderValue` in `Realm` to identify the scheme which it wants to use. By default it uses 'Basic' auth scheme. This can be overriden by realms like KerberosRealm to specify 'Negotiate' scheme or OAuth to specify 'Bearer' or custom realms added by security extensions to specify their own scheme. SAML specifications do not specify anything related to the header but unofficially many have used 'SAML' as auth scheme or used 'Bearer' auth scheme for passing SAML tokens. But most of the realms would use the existing schemes like 'Basic', 'Digest', 'Bearer', 'Negotiate' etc. At the startup, `Security#createComponents` will take care of creating `DefaultAuthenticationFailureHandler` with default response header values for 'WWW-Authenticate' as a list of configured and enabled auth schemes. --- .../DefaultAuthenticationFailureHandler.java | 95 +++++++++++++---- .../xpack/core/security/authc/Realm.java | 15 +++ ...aultAuthenticationFailureHandlerTests.java | 100 ++++++++++++++++++ .../xpack/security/Security.java | 58 +++++++--- .../kerberos/support/KerberosTestCase.java | 2 + 5 files changed, 230 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index 8b31e77f9f8b7..f547a9cf2072f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -10,60 +10,109 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.transport.TransportMessage; +import org.elasticsearch.xpack.core.XPackField; + +import java.util.Collections; +import java.util.List; +import java.util.Map; import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; /** - * The default implementation of a {@link AuthenticationFailureHandler}. This handler will return an exception with a - * RestStatus of 401 and the WWW-Authenticate header with a Basic challenge. + * The default implementation of a {@link AuthenticationFailureHandler}. This + * handler will return an exception with a RestStatus of 401 and default failure + * response headers like 'WWW-Authenticate' */ public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { + private final Map> defaultFailureResponseHeaders; + + /** + * Constructs default authentication failure handler + * + * @deprecated replaced by {@link #DefaultAuthenticationFailureHandler(Map)} + */ + @Deprecated + public DefaultAuthenticationFailureHandler() { + this(null); + } + + /** + * Constructs default authentication failure handler with provided default + * response headers. + * + * @param failureResponseHeaders Map of header key and list of header values to + * be sent as failure response. + * @see Realm#getAuthenticationFailureHeaders() + */ + public DefaultAuthenticationFailureHandler(Map> failureResponseHeaders) { + if (failureResponseHeaders == null || failureResponseHeaders.isEmpty()) { + failureResponseHeaders = Collections.singletonMap("WWW-Authenticate", + Collections.singletonList("Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\"")); + } + this.defaultFailureResponseHeaders = Collections.unmodifiableMap(failureResponseHeaders); + } @Override - public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token, - ThreadContext context) { - return authenticationError("unable to authenticate user [{}] for REST request [{}]", token.principal(), request.uri()); + public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token, ThreadContext context) { + return createAuthenticationError("unable to authenticate user [{}] for REST request [{}]", null, token.principal(), request.uri()); } @Override public ElasticsearchSecurityException failedAuthentication(TransportMessage message, AuthenticationToken token, String action, - ThreadContext context) { - return authenticationError("unable to authenticate user [{}] for action [{}]", token.principal(), action); + ThreadContext context) { + return createAuthenticationError("unable to authenticate user [{}] for action [{}]", null, token.principal(), action); } @Override public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) { - if (e instanceof ElasticsearchSecurityException) { - assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED; - assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1; - return (ElasticsearchSecurityException) e; - } - return authenticationError("error attempting to authenticate request", e); + return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @Override public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e, - ThreadContext context) { - if (e instanceof ElasticsearchSecurityException) { - assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED; - assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1; - return (ElasticsearchSecurityException) e; - } - return authenticationError("error attempting to authenticate request", e); + ThreadContext context) { + return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); } @Override public ElasticsearchSecurityException missingToken(RestRequest request, ThreadContext context) { - return authenticationError("missing authentication token for REST request [{}]", request.uri()); + return createAuthenticationError("missing authentication token for REST request [{}]", null, request.uri()); } @Override public ElasticsearchSecurityException missingToken(TransportMessage message, String action, ThreadContext context) { - return authenticationError("missing authentication token for action [{}]", action); + return createAuthenticationError("missing authentication token for action [{}]", null, action); } @Override public ElasticsearchSecurityException authenticationRequired(String action, ThreadContext context) { - return authenticationError("action [{}] requires authentication", action); + return createAuthenticationError("action [{}] requires authentication", null, action); + } + + /** + * Creates an instance of {@link ElasticsearchSecurityException} with + * {@link RestStatus#UNAUTHORIZED} status. + *

+ * Also adds default failure response headers as configured for this {@link DefaultAuthenticationFailureHandler} + * + * @param message error message + * @param t cause, if it is an instance of + * {@link ElasticsearchSecurityException} asserts status is + * RestStatus.UNAUTHORIZED and adds headers to it, else it will + * create a new instance of {@link ElasticsearchSecurityException} + * @param args error message args + * @return instance of {@link ElasticsearchSecurityException} + */ + private ElasticsearchSecurityException createAuthenticationError(final String message, final Throwable t, final Object... args) { + final ElasticsearchSecurityException ese; + if (t instanceof ElasticsearchSecurityException) { + assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED; + ese = (ElasticsearchSecurityException) t; + } else { + ese = authenticationError(message, t, args); + } + // If it is already present then it will replace the existing header. + defaultFailureResponseHeaders.entrySet().stream().forEach((e) -> ese.addHeader(e.getKey(), e.getValue())); + return ese; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java index 3e92be2ef904d..2c63ca95eb980 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java @@ -8,9 +8,12 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.security.user.User; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -56,6 +59,18 @@ public int order() { return config.order; } + /** + * Each realm can define response headers to be sent on failure. + *

+ * By default it adds 'WWW-Authenticate' header with auth scheme 'Basic'. + * + * @return Map of authentication failure response headers. + */ + public Map> getAuthenticationFailureHeaders() { + return Collections.singletonMap("WWW-Authenticate", + Collections.singletonList("Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\"")); + } + @Override public int compareTo(Realm other) { int result = Integer.compare(config.order, other.config.order); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java new file mode 100644 index 0000000000000..44d186dce0d2f --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java @@ -0,0 +1,100 @@ +/* + * 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.core.security.authc; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackField; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +public class DefaultAuthenticationFailureHandlerTests extends ESTestCase { + + public void testAuthenticationRequired() { + final boolean testDefault = randomBoolean(); + final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; + final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; + final DefaultAuthenticationFailureHandler failuerHandler; + if (testDefault) { + failuerHandler = new DefaultAuthenticationFailureHandler(); + } else { + final Map> failureResponeHeaders = new HashMap<>(); + failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme)); + failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); + } + assertThat(failuerHandler, is(notNullValue())); + final ElasticsearchSecurityException ese = + failuerHandler.authenticationRequired("someaction", new ThreadContext(Settings.builder().build())); + assertThat(ese, is(notNullValue())); + assertThat(ese.getMessage(), equalTo("action [someaction] requires authentication")); + assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); + if (testDefault) { + assertThat(ese.getHeader("WWW-Authenticate").size(), is(1)); + assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme)))); + } else { + assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); + assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + } + } + + public void testExceptionProcessingRequest() { + final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; + final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; + final Map> failureResponeHeaders = new HashMap<>(); + failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme)); + final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); + + assertThat(failuerHandler, is(notNullValue())); + final boolean causeIsElasticsearchSecurityException = randomBoolean(); + final boolean causeIsEseAndUnauthorized = causeIsElasticsearchSecurityException && randomBoolean(); + final ElasticsearchSecurityException eseCause = (causeIsEseAndUnauthorized) + ? new ElasticsearchSecurityException("unauthorized", RestStatus.UNAUTHORIZED, null, (Object[]) null) + : new ElasticsearchSecurityException("different error", RestStatus.BAD_REQUEST, null, (Object[]) null); + final Exception cause = causeIsElasticsearchSecurityException ? eseCause : new Exception("other error"); + eseCause.addHeader("WWW-Authenticate", randomFrom(Arrays.asList(null, ""), Collections.singletonList(basicAuthScheme))); + + if (causeIsElasticsearchSecurityException) { + if (causeIsEseAndUnauthorized) { + final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, + new ThreadContext(Settings.builder().build())); + assertThat(ese, is(notNullValue())); + assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); + assertThat(ese, is(sameInstance(cause))); + assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); + assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + assertThat(ese.getMessage(), equalTo("unauthorized")); + } else { + expectThrows(AssertionError.class, () -> failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, + new ThreadContext(Settings.builder().build()))); + } + } else { + final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, + new ThreadContext(Settings.builder().build())); + assertThat(ese, is(notNullValue())); + assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); + assertThat(ese.getMessage(), equalTo("error attempting to authenticate request")); + assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); + assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + } + + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 5eaf23713b314..f17d4d6849acd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -77,6 +77,7 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestHandler; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; @@ -429,23 +430,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste securityIndex.get().addIndexStateListener(nativeRoleMappingStore::onSecurityIndexStateChange); - AuthenticationFailureHandler failureHandler = null; - String extensionName = null; - for (SecurityExtension extension : securityExtensions) { - AuthenticationFailureHandler extensionFailureHandler = extension.getAuthenticationFailureHandler(); - if (extensionFailureHandler != null && failureHandler != null) { - throw new IllegalStateException("Extensions [" + extensionName + "] and [" + extension.toString() + "] " + - "both set an authentication failure handler"); - } - failureHandler = extensionFailureHandler; - extensionName = extension.toString(); - } - if (failureHandler == null) { - logger.debug("Using default authentication failure handler"); - failureHandler = new DefaultAuthenticationFailureHandler(); - } else { - logger.debug("Using authentication failure handler from extension [" + extensionName + "]"); - } + final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms); authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, anonymousUser, tokenService)); @@ -495,6 +480,45 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste return components; } + private AuthenticationFailureHandler createAuthenticationFailureHandler(final Realms realms) { + AuthenticationFailureHandler failureHandler = null; + String extensionName = null; + for (SecurityExtension extension : securityExtensions) { + AuthenticationFailureHandler extensionFailureHandler = extension.getAuthenticationFailureHandler(); + if (extensionFailureHandler != null && failureHandler != null) { + throw new IllegalStateException("Extensions [" + extensionName + "] and [" + extension.toString() + "] " + + "both set an authentication failure handler"); + } + failureHandler = extensionFailureHandler; + extensionName = extension.toString(); + } + if (failureHandler == null) { + logger.debug("Using default authentication failure handler"); + final Map> defaultFailureResponseHeaders = new HashMap<>(); + realms.asList().stream().forEach((realm) -> { + Map> realmFailureHeaders = realm.getAuthenticationFailureHeaders(); + realmFailureHeaders.entrySet().stream().forEach((e) -> { + String key = e.getKey(); + e.getValue().stream() + .filter(v -> defaultFailureResponseHeaders.computeIfAbsent(key, x -> new ArrayList<>()).contains(v) == false) + .forEach(v -> defaultFailureResponseHeaders.get(key).add(v)); + }); + }); + + if (TokenService.isTokenServiceEnabled(settings)) { + String bearerScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; + if (defaultFailureResponseHeaders.computeIfAbsent("WWW-Authenticate", x -> new ArrayList<>()) + .contains(bearerScheme) == false) { + defaultFailureResponseHeaders.get("WWW-Authenticate").add(bearerScheme); + } + } + failureHandler = new DefaultAuthenticationFailureHandler(defaultFailureResponseHeaders); + } else { + logger.debug("Using authentication failure handler from extension [" + extensionName + "]"); + } + return failureHandler; + } + @Override public Settings additionalSettings() { return additionalSettings(settings, enabled, transportClientMode); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java index 9a9453a8064bd..865688e2b05f5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java @@ -67,6 +67,8 @@ public abstract class KerberosTestCase extends ESTestCase { unsupportedLocaleLanguages.add("ja"); unsupportedLocaleLanguages.add("th"); unsupportedLocaleLanguages.add("hi"); + unsupportedLocaleLanguages.add("uz"); + unsupportedLocaleLanguages.add("fa"); } @BeforeClass From 24a3f1683e78f10b142e6b5417dfae30698d7a1d Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Tue, 10 Jul 2018 11:08:47 +1000 Subject: [PATCH 4/9] [Kerberos] Add Kerberos Realm (#31761) This commit adds authentication realm for handling Kerberos authentication by spnego mechanism. The class `KerberosRealm` authenticates user for given kerberos ticket after validating the ticket using `KerberosTicketValidator`. It uses native role mapping store to find user details and then creates an authenticated `User`. On successful authentication, it will return populated `User` object with roles. On failure to authenticate, it will terminate authentication process with a failure message. The failure could be due to gss context negotiation failure requiring further negotiation and it might return outToken to be communicated with peer as value for header `WWW-Authenticate` in the form 'Negotiate oYH1MIHyoAMK...'. There could be other failures like JAAS login exception or GSS Exception which will terminate the authentication process. As KerberosRealm can terminate authentication process during context negotiation with some outToken, the header value for `WWW-Authenticate` needs to be preserved. Earlier the behavior was to overwrite all the headers as defined in authentication failure handler in my last commit. Negotiate does maintain kind of state over HTTP and so we have to handle this in a special way. For this, I have added a special check for if exception has header 'WWW-Authenticate' with 'Negotiate ' scheme and token, it will not be overwritten. We want Kerberos to be a platinum feature, so it is not included as part of standard types similar to SAML. TODO: Support for user lookup from other realms like AD/LDAP. Authorizing realms feature is work in progress, once completed I will add the support to KerberosRealm. I have a TODO note in source code. --- .../DefaultAuthenticationFailureHandler.java | 29 ++- ...aultAuthenticationFailureHandlerTests.java | 34 ++- .../security/authc/AuthenticationService.java | 4 +- .../xpack/security/authc/InternalRealms.java | 16 +- .../kerberos/KerberosAuthenticationToken.java | 35 +++- .../authc/kerberos/KerberosRealm.java | 193 ++++++++++++++++++ .../support/KerberosTicketValidator.java | 49 +++-- .../authc/AuthenticationServiceTests.java | 56 +++++ .../security/authc/InternalRealmsTests.java | 13 ++ .../xpack/security/authc/RealmsTests.java | 8 +- .../KerberosRealmAuthenticateFailedTests.java | 107 ++++++++++ .../kerberos/KerberosRealmCacheTests.java | 138 +++++++++++++ .../authc/kerberos/KerberosRealmTestCase.java | 136 ++++++++++++ .../authc/kerberos/KerberosRealmTests.java | 93 +++++++++ .../support/KerberosTicketValidatorTests.java | 41 ++-- .../support/SimpleKdcLdapServerTests.java | 11 +- 16 files changed, 894 insertions(+), 69 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index f547a9cf2072f..d6f678a2dcb90 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -93,7 +93,11 @@ public ElasticsearchSecurityException authenticationRequired(String action, Thre * Creates an instance of {@link ElasticsearchSecurityException} with * {@link RestStatus#UNAUTHORIZED} status. *

- * Also adds default failure response headers as configured for this {@link DefaultAuthenticationFailureHandler} + * Also adds default failure response headers as configured for this + * {@link DefaultAuthenticationFailureHandler} + *

+ * It may replace existing response headers if the cause is an instance of + * {@link ElasticsearchSecurityException} * * @param message error message * @param t cause, if it is an instance of @@ -105,14 +109,33 @@ public ElasticsearchSecurityException authenticationRequired(String action, Thre */ private ElasticsearchSecurityException createAuthenticationError(final String message, final Throwable t, final Object... args) { final ElasticsearchSecurityException ese; + final boolean containsNegotiateWithToken; if (t instanceof ElasticsearchSecurityException) { assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED; ese = (ElasticsearchSecurityException) t; + if (ese.getHeader("WWW-Authenticate") != null && ese.getHeader("WWW-Authenticate").isEmpty() == false) { + /** + * If 'WWW-Authenticate' header is present with 'Negotiate ' then do not + * replace. In case of kerberos spnego mechanism, we use + * 'WWW-Authenticate' header value to communicate outToken to peer. + */ + containsNegotiateWithToken = + ese.getHeader("WWW-Authenticate").stream() + .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); + } else { + containsNegotiateWithToken = false; + } } else { ese = authenticationError(message, t, args); + containsNegotiateWithToken = false; } - // If it is already present then it will replace the existing header. - defaultFailureResponseHeaders.entrySet().stream().forEach((e) -> ese.addHeader(e.getKey(), e.getValue())); + defaultFailureResponseHeaders.entrySet().stream().forEach((e) -> { + if (containsNegotiateWithToken && e.getKey().equalsIgnoreCase("WWW-Authenticate")) { + return; + } + // If it is already present then it will replace the existing header. + ese.addHeader(e.getKey(), e.getValue()); + }); return ese; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java index 44d186dce0d2f..2598461c37280 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java @@ -48,19 +48,18 @@ public void testAuthenticationRequired() { assertThat(ese.getMessage(), equalTo("action [someaction] requires authentication")); assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); if (testDefault) { - assertThat(ese.getHeader("WWW-Authenticate").size(), is(1)); - assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme)))); + assertWWWAuthenticateWithSchemes(ese, basicAuthScheme); } else { - assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); - assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme); } } public void testExceptionProcessingRequest() { final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; + final String negotiateAuthScheme = randomFrom("Negotiate", "Negotiate Ijoijksdk"); final Map> failureResponeHeaders = new HashMap<>(); - failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme)); + failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme)); final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); assertThat(failuerHandler, is(notNullValue())); @@ -70,7 +69,11 @@ public void testExceptionProcessingRequest() { ? new ElasticsearchSecurityException("unauthorized", RestStatus.UNAUTHORIZED, null, (Object[]) null) : new ElasticsearchSecurityException("different error", RestStatus.BAD_REQUEST, null, (Object[]) null); final Exception cause = causeIsElasticsearchSecurityException ? eseCause : new Exception("other error"); - eseCause.addHeader("WWW-Authenticate", randomFrom(Arrays.asList(null, ""), Collections.singletonList(basicAuthScheme))); + final boolean withAuthenticateHeader = randomBoolean(); + final String selectedScheme = randomFrom(bearerAuthScheme, basicAuthScheme, negotiateAuthScheme); + if (withAuthenticateHeader) { + eseCause.addHeader("WWW-Authenticate", Collections.singletonList(selectedScheme)); + } if (causeIsElasticsearchSecurityException) { if (causeIsEseAndUnauthorized) { @@ -79,8 +82,15 @@ public void testExceptionProcessingRequest() { assertThat(ese, is(notNullValue())); assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); assertThat(ese, is(sameInstance(cause))); - assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); - assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + if (withAuthenticateHeader == false) { + assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); + } else { + if (selectedScheme.contains("Negotiate ")) { + assertWWWAuthenticateWithSchemes(ese, selectedScheme); + } else { + assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); + } + } assertThat(ese.getMessage(), equalTo("unauthorized")); } else { expectThrows(AssertionError.class, () -> failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, @@ -92,9 +102,13 @@ public void testExceptionProcessingRequest() { assertThat(ese, is(notNullValue())); assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); assertThat(ese.getMessage(), equalTo("error attempting to authenticate request")); - assertThat(ese.getHeader("WWW-Authenticate").size(), is(2)); - assertThat(ese.getHeader("WWW-Authenticate"), contains(Arrays.asList(equalTo(basicAuthScheme), equalTo(bearerAuthScheme)))); + assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); } } + + private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) { + assertThat(ese.getHeader("WWW-Authenticate").size(), is(schemes.length)); + assertThat(ese.getHeader("WWW-Authenticate"), contains(schemes)); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 8bae951e88360..069516bcd2b6d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -271,7 +271,9 @@ private void consumeToken(AuthenticationToken token) { if (result.getStatus() == AuthenticationResult.Status.TERMINATE) { logger.info("Authentication of [{}] was terminated by realm [{}] - {}", authenticationToken.principal(), realm.name(), result.getMessage()); - userListener.onFailure(Exceptions.authenticationError(result.getMessage(), result.getException())); + Exception e = (result.getException() != null) ? result.getException() + : Exceptions.authenticationError(result.getMessage()); + userListener.onFailure(e); } else { if (result.getMessage() != null) { messages.put(realm, new Tuple<>(result.getMessage(), result.getException())); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index d8d0d26f99e0d..d568a052a5e15 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; @@ -24,6 +25,7 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.file.FileRealm; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; import org.elasticsearch.xpack.security.authc.pki.PkiRealm; import org.elasticsearch.xpack.security.authc.saml.SamlRealm; @@ -32,10 +34,8 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -50,17 +50,16 @@ public final class InternalRealms { /** * The list of all internal realm types, excluding {@link ReservedRealm#TYPE}. */ - private static final Set XPACK_TYPES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, - SamlRealmSettings.TYPE - ))); + private static final Set XPACK_TYPES = Collections + .unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, + LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE, SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE)); /** * The list of all standard realm types, which are those provided by x-pack and do not have extensive * interaction with third party sources */ - private static final Set STANDARD_TYPES = - Collections.unmodifiableSet(Sets.difference(XPACK_TYPES, Collections.singleton(SamlRealmSettings.TYPE))); + private static final Set STANDARD_TYPES = Collections.unmodifiableSet(Sets.newHashSet(NativeRealmSettings.TYPE, + FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, PkiRealmSettings.TYPE)); /** * Determines whether type is an internal realm-type that is provided by x-pack, @@ -105,6 +104,7 @@ public static Map getFactories(ThreadPool threadPool, Res sslService, resourceWatcherService, nativeRoleMappingStore, threadPool)); map.put(PkiRealmSettings.TYPE, config -> new PkiRealm(config, resourceWatcherService, nativeRoleMappingStore)); map.put(SamlRealmSettings.TYPE, config -> SamlRealm.create(config, sslService, resourceWatcherService, nativeRoleMappingStore)); + map.put(KerberosRealmSettings.TYPE, config -> new KerberosRealm(config, nativeRoleMappingStore, threadPool)); return Collections.unmodifiableMap(map); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java index e78624ee687d7..1a330bd2ddd54 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java @@ -109,9 +109,42 @@ public boolean equals(final Object other) { return Arrays.equals(otherKerbToken.decodedToken, this.decodedToken); } - private static ElasticsearchSecurityException unauthorized(final String message, final Throwable cause, final Object... args) { + /** + * Creates {@link ElasticsearchSecurityException} with + * {@link RestStatus#UNAUTHORIZED} and cause. This also populates + * 'WWW-Authenticate' header with value as 'Negotiate' scheme. + * + * @param message the detail message + * @param cause nested exception + * @param args the arguments for the message + * @return instance of {@link ElasticsearchSecurityException} + */ + static ElasticsearchSecurityException unauthorized(final String message, final Throwable cause, final Object... args) { ElasticsearchSecurityException ese = new ElasticsearchSecurityException(message, RestStatus.UNAUTHORIZED, cause, args); ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_SCHEME_NAME); return ese; } + + /** + * Sets 'WWW-Authenticate' header if outToken is not null on passed instance of + * {@link ElasticsearchSecurityException} and returns the instance.
+ * If outToken is provided and is not {@code null} or empty, then that is + * appended to 'Negotiate ' and is used as header value for header + * 'WWW-Authenticate' sent to the peer in the form 'Negotiate oYH1MIHyoAMK...'. + * This is required by client for GSS negotiation to continue further. + * + * @param ese instance of {@link ElasticsearchSecurityException} with status + * {@link RestStatus#UNAUTHORIZED} + * @param outToken if non {@code null} and not empty then this will be the value + * sent to the peer. + * @return instance of {@link ElasticsearchSecurityException} with + * 'WWW-Authenticate' header populated. + */ + static ElasticsearchSecurityException unauthorizedWithOutputToken(final ElasticsearchSecurityException ese, final String outToken) { + assert ese.status() == RestStatus.UNAUTHORIZED; + if (Strings.hasText(outToken)) { + ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken); + } + return ese; + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java new file mode 100644 index 0000000000000..81daa07fe7760 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java @@ -0,0 +1,193 @@ +/* + * 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.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; +import org.elasticsearch.xpack.security.authc.support.CachingRealm; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; +import org.ietf.jgss.GSSException; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.security.auth.login.LoginException; + +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.AUTH_HEADER; +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX; +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.NEGOTIATE_SCHEME_NAME; +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.WWW_AUTHENTICATE; +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.unauthorized; +import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.unauthorizedWithOutputToken; + +/** + * This class provides support for Kerberos authentication using spnego + * mechanism. + *

+ * It provides support to extract kerberos ticket using + * {@link KerberosAuthenticationToken#extractToken(String)} to build + * {@link KerberosAuthenticationToken} and then authenticating user when + * {@link KerberosTicketValidator} validates the ticket. + *

+ * On successful authentication, it will build {@link User} object populated + * with roles and will return {@link AuthenticationResult} with user object. On + * authentication failure, it will return {@link AuthenticationResult} with + * status to terminate authentication process. + */ +public final class KerberosRealm extends Realm implements CachingRealm { + + private final Cache userPrincipalNameToUserCache; + private final NativeRoleMappingStore userRoleMapper; + private final KerberosTicketValidator kerberosTicketValidator; + private final ThreadPool threadPool; + private final Path keytabPath; + private final boolean enableKerberosDebug; + + public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, final ThreadPool threadPool) { + this(config, nativeRoleMappingStore, new KerberosTicketValidator(), threadPool, null); + } + + // pkg scoped for testing + KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, + final KerberosTicketValidator kerberosTicketValidator, final ThreadPool threadPool, + final Cache userPrincipalNameToUserCache) { + super(KerberosRealmSettings.TYPE, config); + this.userRoleMapper = nativeRoleMappingStore; + this.userRoleMapper.refreshRealmOnChange(this); + final TimeValue ttl = KerberosRealmSettings.CACHE_TTL_SETTING.get(config.settings()); + if (ttl.getNanos() > 0) { + this.userPrincipalNameToUserCache = (userPrincipalNameToUserCache == null) + ? CacheBuilder.builder() + .setExpireAfterWrite(KerberosRealmSettings.CACHE_TTL_SETTING.get(config.settings())) + .setMaximumWeight(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())).build() + : userPrincipalNameToUserCache; + } else { + this.userPrincipalNameToUserCache = null; + } + this.kerberosTicketValidator = kerberosTicketValidator; + this.threadPool = threadPool; + this.keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + this.enableKerberosDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + } + + @Override + public Map> getAuthenticationFailureHeaders() { + return Collections.singletonMap(WWW_AUTHENTICATE, Collections.singletonList(NEGOTIATE_SCHEME_NAME)); + } + + @Override + public void expire(final String username) { + if (userPrincipalNameToUserCache != null) { + userPrincipalNameToUserCache.invalidate(username); + } + } + + @Override + public void expireAll() { + if (userPrincipalNameToUserCache != null) { + userPrincipalNameToUserCache.invalidateAll(); + } + } + + @Override + public boolean supports(final AuthenticationToken token) { + return token instanceof KerberosAuthenticationToken; + } + + @Override + public AuthenticationToken token(final ThreadContext context) { + return KerberosAuthenticationToken.extractToken(context.getHeader(AUTH_HEADER)); + } + + @Override + public void authenticate(final AuthenticationToken token, final ActionListener listener) { + assert token instanceof KerberosAuthenticationToken; + final KerberosAuthenticationToken kerbAuthnToken = (KerberosAuthenticationToken) token; + kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug, + ActionListener.wrap(userPrincipalNameOutToken -> { + if (userPrincipalNameOutToken.v1() != null) { + buildUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener); + } else { + /** + * This is when security context could not be established may be due to ongoing + * negotiation and requires token to be sent back to peer for continuing + * further. We are terminating the authentication process as this is spengo + * negotiation and no other realm can handle this. We can have only one Kerberos + * realm in the system so terminating with RestStatus Unauthorized (401) and + * with 'WWW-Authenticate' header populated with value with token in the form + * 'Negotiate oYH1MIHyoAMK...' + */ + String errorMessage = "failed to authenticate user, gss context negotiation not complete"; + ElasticsearchSecurityException ese = unauthorized(errorMessage, null); + ese = unauthorizedWithOutputToken(ese, userPrincipalNameOutToken.v2()); + listener.onResponse(AuthenticationResult.terminate(errorMessage, ese)); + } + }, e -> handleException(e, listener))); + } + + private void handleException(Exception e, final ActionListener listener) { + if (e instanceof LoginException) { + listener.onResponse(AuthenticationResult.terminate("failed to authenticate user, service login failure", + unauthorized(e.getLocalizedMessage(), e))); + } else if (e instanceof GSSException) { + listener.onResponse(AuthenticationResult.terminate("failed to authenticate user, gss context negotiation failure", + unauthorized(e.getLocalizedMessage(), e))); + } else { + listener.onFailure(e); + } + } + + private void buildUser(final String username, final String outToken, final ActionListener listener) { + // if outToken is present then it needs to be communicated with peer, add it to + // response header in thread context. + if (Strings.hasText(outToken)) { + threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken); + } + final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null; + if (user != null) { + /** + * TODO: bizybot If authorizing realms configured, resolve user from those + * realms and then return. + */ + listener.onResponse(AuthenticationResult.success(user)); + } else { + /** + * TODO: bizybot If authorizing realms configured, resolve user from those + * realms, cache it and then return. + */ + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config); + userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { + final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + if (userPrincipalNameToUserCache != null) { + userPrincipalNameToUserCache.put(username, computedUser); + } + listener.onResponse(AuthenticationResult.success(computedUser)); + }, listener::onFailure)); + } + } + + @Override + public void lookupUser(final String username, final ActionListener listener) { + listener.onResponse(null); + } +} \ No newline at end of file diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java index 61e8f7500d5f5..3e837bd6377eb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.ESLoggerFactory; import org.ietf.jgss.GSSContext; @@ -63,25 +64,22 @@ private static Oid getSpnegoOid() { * First performs service login using keytab, supports multiple principals in * keytab and the principal is selected based on the request. *

- * The GSS security context establishment is handled and if it is established then - * returns the Tuple of username and out token for peer reply. It may return - * Tuple with null username but with a outToken to be sent to peer for further - * negotiation. + * The GSS security context establishment state is handled as follows:
+ * If the context is established it will call {@link ActionListener#onResponse} + * with a {@link Tuple} of username and outToken for peer reply.
+ * If the context is not established then it will call + * {@link ActionListener#onResponse} with a Tuple where username is null but + * with a outToken that needs to be sent to peer for further negotiation.
+ * Never calls {@link ActionListener#onResponse} with a {@code null} tuple.
+ * On failure, it will call {@link ActionListener#onFailure(Exception)} * * @param decodedToken base64 decoded kerberos ticket bytes * @param keytabPath Path to Service key tab file containing credentials for ES * service. * @param krbDebug if {@code true} enables jaas krb5 login module debug logs. - * @return {@link Tuple} of user name {@link GSSContext#getSrcName()} and out - * token base64 encoded if any. When context is not yet established user - * name is {@code null}. - * @throws LoginException thrown when service authentication fails - * {@link LoginContext#login()} - * @throws GSSException thrown when GSS Context negotiation fails - * {@link GSSException} */ - public Tuple validateTicket(final byte[] decodedToken, final Path keytabPath, final boolean krbDebug) - throws LoginException, GSSException { + public void validateTicket(final byte[] decodedToken, final Path keytabPath, final boolean krbDebug, + final ActionListener> actionListener) { final GSSManager gssManager = GSSManager.getInstance(); GSSContext gssContext = null; LoginContext loginContext = null; @@ -92,15 +90,17 @@ public Tuple validateTicket(final byte[] decodedToken, final Pat final String base64OutToken = encodeToString(acceptSecContext(decodedToken, gssContext, loginContext.getSubject())); LOGGER.trace("validateTicket isGSSContextEstablished = {}, username = {}, outToken = {}", gssContext.isEstablished(), gssContext.getSrcName().toString(), base64OutToken); - return new Tuple<>(gssContext.isEstablished() ? gssContext.getSrcName().toString() : null, base64OutToken); + actionListener.onResponse(new Tuple<>(gssContext.isEstablished() ? gssContext.getSrcName().toString() : null, base64OutToken)); + } catch (GSSException e) { + actionListener.onFailure(e); } catch (PrivilegedActionException pve) { if (pve.getCause() instanceof LoginException) { - throw (LoginException) pve.getCause(); + actionListener.onFailure((LoginException) pve.getCause()); } if (pve.getCause() instanceof GSSException) { - throw (GSSException) pve.getCause(); + actionListener.onFailure((GSSException) pve.getCause()); } - throw ExceptionsHelper.convertToRuntime(pve.getException()); + actionListener.onFailure(pve.getException()); } finally { privilegedLogoutNoThrow(loginContext); privilegedDisposeNoThrow(gssContext); @@ -148,13 +148,11 @@ private static byte[] acceptSecContext(final byte[] base64decodedTicket, final G * @param gssManager {@link GSSManager} * @param subject logged in {@link Subject} * @return {@link GSSCredential} for particular mechanism - * @throws GSSException * @throws PrivilegedActionException */ - private static GSSCredential createCredentials(final GSSManager gssManager, - final Subject subject) throws GSSException, PrivilegedActionException { - return doAsWrapper(subject, (PrivilegedExceptionAction) () -> gssManager - .createCredential(null, GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.ACCEPT_ONLY)); + private static GSSCredential createCredentials(final GSSManager gssManager, final Subject subject) throws PrivilegedActionException { + return doAsWrapper(subject, (PrivilegedExceptionAction) () -> gssManager.createCredential(null, + GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.ACCEPT_ONLY)); } /** @@ -225,8 +223,7 @@ private static void privilegedLogoutNoThrow(final LoginContext loginContext) { * closed using {@link LoginContext#logout()} after usage. * @throws PrivilegedActionException when privileged action threw exception */ - private static LoginContext serviceLogin(final String keytabFilePath, final boolean krbDebug) - throws PrivilegedActionException { + private static LoginContext serviceLogin(final String keytabFilePath, final boolean krbDebug) throws PrivilegedActionException { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { final Subject subject = new Subject(false, Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); final Configuration conf = new KeytabJaasConf(keytabFilePath, krbDebug); @@ -238,8 +235,8 @@ private static LoginContext serviceLogin(final String keytabFilePath, final bool /** * 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 - * avoid additional configuration required from the user. + * 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. diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index bb32ed699950c..fda87d0340b8a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -49,6 +49,7 @@ import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportMessage; +import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; @@ -88,6 +89,7 @@ import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; import static org.elasticsearch.xpack.security.authc.TokenServiceTests.mockGetTokenFromId; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -618,6 +620,47 @@ public void testRealmSupportsMethodThrowingExceptionRest() throws Exception { } } + public void testRealmAuthenticateTerminatingAuthenticationProcess() throws Exception { + final AuthenticationToken token = mock(AuthenticationToken.class); + when(secondRealm.token(threadContext)).thenReturn(token); + when(secondRealm.supports(token)).thenReturn(true); + final boolean terminateWithNoException = rarely(); + final boolean throwElasticsearchSecurityException = (terminateWithNoException == false) && randomBoolean(); + final boolean withAuthenticateHeader = throwElasticsearchSecurityException && randomBoolean(); + Exception throwE = new Exception("general authentication error"); + final String basicScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; + String selectedScheme = randomFrom(basicScheme, "Negotiate IOJoj"); + if (throwElasticsearchSecurityException) { + throwE = new ElasticsearchSecurityException("authentication error", RestStatus.UNAUTHORIZED); + if (withAuthenticateHeader) { + ((ElasticsearchSecurityException) throwE).addHeader("WWW-Authenticate", selectedScheme); + } + } + mockAuthenticate(secondRealm, token, (terminateWithNoException) ? null : throwE, true); + + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", message, null)); + if (terminateWithNoException) { + assertThat(e.getMessage(), is("terminate authc process")); + assertThat(e.getHeader("WWW-Authenticate"), contains(basicScheme)); + } else { + if (throwElasticsearchSecurityException) { + assertThat(e.getMessage(), is("authentication error")); + if (withAuthenticateHeader) { + assertThat(e.getHeader("WWW-Authenticate"), contains(selectedScheme)); + } else { + assertThat(e.getHeader("WWW-Authenticate"), contains(basicScheme)); + } + } else { + assertThat(e.getMessage(), is("error attempting to authenticate request")); + assertThat(e.getHeader("WWW-Authenticate"), contains(basicScheme)); + } + } + verify(auditTrail).authenticationFailed(secondRealm.name(), token, "_action", message); + verify(auditTrail).authenticationFailed(token, "_action", message); + verifyNoMoreInteractions(auditTrail); + } + public void testRealmAuthenticateThrowingException() throws Exception { AuthenticationToken token = mock(AuthenticationToken.class); when(secondRealm.token(threadContext)).thenReturn(token); @@ -998,6 +1041,19 @@ private void mockAuthenticate(Realm realm, AuthenticationToken token, User user) }).when(realm).authenticate(eq(token), any(ActionListener.class)); } + @SuppressWarnings("unchecked") + private void mockAuthenticate(Realm realm, AuthenticationToken token, Exception e, boolean terminate) { + doAnswer((i) -> { + ActionListener listener = (ActionListener) i.getArguments()[1]; + if (terminate) { + listener.onResponse(AuthenticationResult.terminate("terminate authc process", e)); + } else { + listener.onResponse(AuthenticationResult.unsuccessful("unsuccessful, but continue authc process", e)); + } + return null; + }).when(realm).authenticate(eq(token), any(ActionListener.class)); + } + private Authentication authenticateBlocking(RestRequest restRequest) { PlainActionFuture future = new PlainActionFuture<>(); service.authenticate(restRequest, future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java index 0cbeced00b2ab..2e91c40677ed6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/InternalRealmsTests.java @@ -14,6 +14,11 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; @@ -49,4 +54,12 @@ public void testNativeRealmRegistersIndexHealthChangeListener() throws Exception TestEnvironment.newEnvironment(settings), new ThreadContext(settings))); verify(securityIndex, times(2)).addIndexStateListener(isA(BiConsumer.class)); } + + public void testIsStandardType() { + String type = randomFrom(NativeRealmSettings.TYPE, FileRealmSettings.TYPE, LdapRealmSettings.AD_TYPE, LdapRealmSettings.LDAP_TYPE, + PkiRealmSettings.TYPE); + assertThat(InternalRealms.isStandardRealm(type), is(true)); + type = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE); + assertThat(InternalRealms.isStandardRealm(type), is(false)); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java index ff4c30ddf8c0a..a71f5cb1cf761 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.security.user.User; @@ -335,10 +336,11 @@ public void testUnlicensedWithNativeRealmSettingss() throws Exception { } public void testUnlicensedWithNonStandardRealms() throws Exception { - factories.put(SamlRealmSettings.TYPE, config -> new DummyRealm(SamlRealmSettings.TYPE, config)); + final String selectedRealmType = randomFrom(SamlRealmSettings.TYPE, KerberosRealmSettings.TYPE); + factories.put(selectedRealmType, config -> new DummyRealm(selectedRealmType, config)); Settings.Builder builder = Settings.builder() .put("path.home", createTempDir()) - .put("xpack.security.authc.realms.foo.type", SamlRealmSettings.TYPE) + .put("xpack.security.authc.realms.foo.type", selectedRealmType) .put("xpack.security.authc.realms.foo.order", "0"); Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); @@ -349,7 +351,7 @@ public void testUnlicensedWithNonStandardRealms() throws Exception { assertThat(realm, is(reservedRealm)); assertThat(iter.hasNext(), is(true)); realm = iter.next(); - assertThat(realm.type(), is(SamlRealmSettings.TYPE)); + assertThat(realm.type(), is(selectedRealmType)); assertThat(iter.hasNext(), is(false)); when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.DEFAULT); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java new file mode 100644 index 0000000000000..144bec3d58315 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java @@ -0,0 +1,107 @@ +/* + * 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.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.user.User; +import org.ietf.jgss.GSSException; + +import java.nio.file.Path; +import java.util.List; + +import javax.security.auth.login.LoginException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; + +public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase { + + public void testAuthenticateWithNonKerberosAuthenticationToken() { + final KerberosRealm kerberosRealm = createKerberosRealm(randomAlphaOfLength(5)); + + final UsernamePasswordToken usernamePasswordToken = + new UsernamePasswordToken(randomAlphaOfLength(5), new SecureString(new char[] { 'a', 'b', 'c' })); + expectThrows(AssertionError.class, () -> kerberosRealm.authenticate(usernamePasswordToken, PlainActionFuture.newFuture())); + } + + public void testAuthenticateDifferentFailureScenarios() throws LoginException, GSSException { + final String username = randomAlphaOfLength(5); + final String outToken = randomAlphaOfLength(10); + final KerberosRealm kerberosRealm = createKerberosRealm(username); + final boolean validTicket = rarely(); + final boolean throwExceptionForInvalidTicket = validTicket ? false : randomBoolean(); + final boolean throwLoginException = randomBoolean(); + final byte[] decodedTicket = randomByteArrayOfLength(5); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + if (validTicket) { + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, outToken), null); + } else { + if (throwExceptionForInvalidTicket) { + if (throwLoginException) { + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, null, new LoginException("Login Exception")); + } else { + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, null, new GSSException(GSSException.FAILURE)); + } + } else { + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(null, outToken), null); + } + } + final boolean nullKerberosAuthnToken = rarely(); + final KerberosAuthenticationToken kerberosAuthenticationToken = + nullKerberosAuthnToken ? null : new KerberosAuthenticationToken(decodedTicket); + + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + AuthenticationResult result = future.actionGet(); + assertThat(result, is(notNullValue())); + if (nullKerberosAuthnToken) { + assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE))); + } else { + if (validTicket) { + final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + assertSuccessAuthenticationResult(expectedUser, outToken, result); + } else { + assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE))); + if (throwExceptionForInvalidTicket == false) { + assertThat(result.getException(), is(instanceOf(ElasticsearchSecurityException.class))); + final List wwwAuthnHeader = ((ElasticsearchSecurityException) result.getException()) + .getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); + assertThat(wwwAuthnHeader, is(notNullValue())); + assertThat(wwwAuthnHeader.get(0), is(equalTo(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + outToken))); + assertThat(result.getMessage(), is(equalTo("failed to authenticate user, gss context negotiation not complete"))); + } else { + if (throwLoginException) { + assertThat(result.getMessage(), is(equalTo("failed to authenticate user, service login failure"))); + } else { + assertThat(result.getMessage(), is(equalTo("failed to authenticate user, gss context negotiation failure"))); + } + assertThat(result.getException(), is(instanceOf(ElasticsearchSecurityException.class))); + final List wwwAuthnHeader = ((ElasticsearchSecurityException) result.getException()) + .getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); + assertThat(wwwAuthnHeader, is(notNullValue())); + assertThat(wwwAuthnHeader.get(0), is(equalTo(KerberosAuthenticationToken.NEGOTIATE_SCHEME_NAME))); + } + } + verify(mockKerberosTicketValidator).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java new file mode 100644 index 0000000000000..24712e53b504e --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -0,0 +1,138 @@ +/* + * 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.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; +import org.ietf.jgss.GSSException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import javax.security.auth.login.LoginException; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class KerberosRealmCacheTests extends KerberosRealmTestCase { + + public void testAuthenticateWithCache() throws LoginException, GSSException { + final String username = randomAlphaOfLength(5); + final String outToken = randomAlphaOfLength(10); + final KerberosRealm kerberosRealm = createKerberosRealm(username); + + final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final byte[] decodedTicket = randomByteArrayOfLength(10); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, outToken), null); + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + + // authenticate + final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + // authenticate with cache + final User user2 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + + assertThat(user1, sameInstance(user2)); + verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + verify(mockNativeRoleMappingStore).resolveRoles(any(UserData.class), any(ActionListener.class)); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + } + + public void testCacheInvalidationScenarios() throws LoginException, GSSException { + final String outToken = randomAlphaOfLength(10); + final List userNames = Arrays.asList(randomAlphaOfLength(5) + "@REALM", randomAlphaOfLength(5) + "@REALM"); + final KerberosRealm kerberosRealm = createKerberosRealm(userNames.toArray(new String[0])); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + + final String authNUsername = randomFrom(userNames); + final byte[] decodedTicket = randomByteArrayOfLength(10); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(authNUsername, outToken), null); + final User expectedUser = new User(authNUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + + final String expireThisUser = randomFrom(userNames); + boolean expireAll = randomBoolean(); + if (expireAll) { + kerberosRealm.expireAll(); + } else { + kerberosRealm.expire(expireThisUser); + } + + final User user2 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + + if (expireAll || expireThisUser.equals(authNUsername)) { + assertThat(user1, is(not(sameInstance(user2)))); + verify(mockNativeRoleMappingStore, times(2)).resolveRoles(any(UserData.class), any(ActionListener.class)); + } else { + assertThat(user1, sameInstance(user2)); + verify(mockNativeRoleMappingStore).resolveRoles(any(UserData.class), any(ActionListener.class)); + } + verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + } + + public void testAuthenticateWithValidTicketSucessAuthnWithUserDetailsWhenCacheDisabled() + throws LoginException, GSSException, IOException { + final String username = randomAlphaOfLength(5); + final String outToken = randomAlphaOfLength(10); + // if cache.ttl <= 0 then the cache is disabled + settings = KerberosTestCase.buildKerberosRealmSettings( + KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true); + final KerberosRealm kerberosRealm = createKerberosRealm(username); + + final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final byte[] decodedTicket = randomByteArrayOfLength(10); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, outToken), null); + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + + // authenticate + final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + // authenticate when cache has been disabled + final User user2 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); + + assertThat(user1, not(sameInstance(user2))); + verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + verify(mockNativeRoleMappingStore, times(2)).resolveRoles(any(UserData.class), any(ActionListener.class)); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + } + + private User authenticateAndAssertResult(final KerberosRealm kerberosRealm, final User expectedUser, + final KerberosAuthenticationToken kerberosAuthenticationToken, String outToken) { + final PlainActionFuture future = PlainActionFuture.newFuture(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + final AuthenticationResult result = future.actionGet(); + assertSuccessAuthenticationResult(expectedUser, outToken, result); + return result.getUser(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java new file mode 100644 index 0000000000000..85e0d88707216 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -0,0 +1,136 @@ +/* + * 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 com.google.common.collect.Sets; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.support.Exceptions; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.junit.After; +import org.junit.Before; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public abstract class KerberosRealmTestCase extends ESTestCase { + + protected Path dir; + protected ThreadPool threadPool; + protected Settings globalSettings; + protected ResourceWatcherService resourceWatcherService; + protected Settings settings; + protected RealmConfig config; + + protected KerberosTicketValidator mockKerberosTicketValidator; + protected NativeRoleMappingStore mockNativeRoleMappingStore; + + protected static final Set roles = Sets.newHashSet("admin", "kibana_user"); + + @Before + public void setup() throws Exception { + threadPool = new TestThreadPool("kerb realm tests"); + resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); + dir = createTempDir(); + globalSettings = Settings.builder().put("path.home", dir).build(); + settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString()); + } + + @After + public void shutdown() throws InterruptedException { + resourceWatcherService.stop(); + terminate(threadPool); + } + + protected void mockKerberosTicketValidator(final byte[] decodedTicket, final Path keytabPath, final boolean krbDebug, + final Tuple value, final Exception e) { + assert value != null || e != null; + doAnswer((i) -> { + ActionListener> listener = (ActionListener>) i.getArguments()[3]; + if (e != null) { + listener.onFailure(e); + } else { + listener.onResponse(value); + } + return null; + }).when(mockKerberosTicketValidator).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), any(ActionListener.class)); + } + + protected void assertSuccessAuthenticationResult(final User expectedUser, final String outToken, final AuthenticationResult result) { + assertThat(result, is(notNullValue())); + assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.SUCCESS))); + assertThat(result.getUser(), is(equalTo(expectedUser))); + final Map> responseHeaders = threadPool.getThreadContext().getResponseHeaders(); + assertThat(responseHeaders, is(notNullValue())); + assertThat(responseHeaders.get(KerberosAuthenticationToken.WWW_AUTHENTICATE).get(0), + is(equalTo(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + outToken))); + } + + protected KerberosRealm createKerberosRealm(final String... userForRoleMapping) { + config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), + new ThreadContext(globalSettings)); + mockNativeRoleMappingStore = roleMappingStore(Arrays.asList(userForRoleMapping)); + mockKerberosTicketValidator = mock(KerberosTicketValidator.class); + final KerberosRealm kerberosRealm = + new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null); + return kerberosRealm; + } + + @SuppressWarnings("unchecked") + protected NativeRoleMappingStore roleMappingStore(final List expectedUserNames) { + final Client mockClient = mock(Client.class); + when(mockClient.threadPool()).thenReturn(threadPool); + when(mockClient.settings()).thenReturn(settings); + + final NativeRoleMappingStore store = new NativeRoleMappingStore(Settings.EMPTY, mockClient, mock(SecurityIndexManager.class)); + final NativeRoleMappingStore roleMapper = spy(store); + + doAnswer(invocation -> { + final UserRoleMapper.UserData userData = (UserRoleMapper.UserData) invocation.getArguments()[0]; + final ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + if (expectedUserNames.contains(userData.getUsername())) { + listener.onResponse(roles); + } else { + listener.onFailure( + Exceptions.authorizationError("Expected UPN '" + expectedUserNames + "' but was '" + userData.getUsername() + "'")); + } + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + + return roleMapper; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java new file mode 100644 index 0000000000000..c504f79233b22 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -0,0 +1,93 @@ +/* + * 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.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; +import org.ietf.jgss.GSSException; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; + +import javax.security.auth.login.LoginException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class KerberosRealmTests extends KerberosRealmTestCase { + + public void testSupports() { + final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(randomByteArrayOfLength(5)); + assertThat(kerberosRealm.supports(kerberosAuthenticationToken), is(true)); + final UsernamePasswordToken usernamePasswordToken = + new UsernamePasswordToken(randomAlphaOfLength(5), new SecureString(new char[] { 'a', 'b', 'c' })); + assertThat(kerberosRealm.supports(usernamePasswordToken), is(false)); + } + + public void testAuthenticateWithValidTicketSucessAuthnWithUserDetails() throws LoginException, GSSException { + final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + + final User expectedUser = new User("test@REALM", roles.toArray(new String[roles.size()]), null, null, null, true); + final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>("test@REALM", "out-token"), null); + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet()); + + verify(mockKerberosTicketValidator, times(1)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + verify(mockNativeRoleMappingStore).resolveRoles(any(UserData.class), any(ActionListener.class)); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + } + + public void testFailedAuthorization() throws LoginException, GSSException { + final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); + final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>("does-not-exist@REALM", "out-token"), null); + + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(new KerberosAuthenticationToken(decodedTicket), future); + + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); + assertThat(e.status(), is(RestStatus.FORBIDDEN)); + assertThat(e.getMessage(), equalTo("Expected UPN '" + Arrays.asList("test@REALM") + "' but was 'does-not-exist@REALM'")); + } + + public void testLookupUser() { + final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.lookupUser("test@REALM", future); + assertThat(future.actionGet(), is(nullValue())); + } + +} \ No newline at end of file diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java index 602bb1265fe11..16690ec3cc304 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java @@ -6,8 +6,10 @@ package org.elasticsearch.xpack.security.authc.kerberos.support; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; @@ -17,6 +19,7 @@ import java.nio.file.Path; import java.security.PrivilegedActionException; import java.util.Base64; +import java.util.concurrent.ExecutionException; import javax.security.auth.login.LoginException; @@ -41,8 +44,9 @@ public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Ex final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - final GSSException gssException = expectThrows(GSSException.class, - () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + final PlainActionFuture> future = new PlainActionFuture<>(); + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, future); + final GSSException gssException = expectThrows(GSSException.class, () -> unwrapExpectedExceptionFromFutureAndThrow(future)); assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); } } @@ -52,8 +56,9 @@ public void testInvalidKerbTicketFailsValidation() throws Exception { final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - final GSSException gssException = expectThrows(GSSException.class, - () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + final PlainActionFuture> future = new PlainActionFuture<>(); + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, future); + final GSSException gssException = expectThrows(GSSException.class, () -> unwrapExpectedExceptionFromFutureAndThrow(future)); assertThat(gssException.getMajor(), equalTo(GSSException.DEFECTIVE_TOKEN)); } @@ -70,8 +75,9 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() settings = buildKerberosRealmSettings(ktabPath.toString()); final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - final GSSException gssException = expectThrows(GSSException.class, - () -> kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true)); + final PlainActionFuture> future = new PlainActionFuture<>(); + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, future); + final GSSException gssException = expectThrows(GSSException.class, () -> unwrapExpectedExceptionFromFutureAndThrow(future)); assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); } } @@ -86,16 +92,27 @@ public void testValidKebrerosTicket() throws PrivilegedActionException, GSSExcep final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - final Tuple userNameOutToken = - kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true); - assertThat(userNameOutToken, is(notNullValue())); - assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); - assertThat(userNameOutToken.v2(), is(notNullValue())); + final PlainActionFuture> future = new PlainActionFuture<>(); + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, future); + assertThat(future.actionGet(), is(notNullValue())); + assertThat(future.actionGet().v1(), equalTo(principalName(clientUserName))); + assertThat(future.actionGet().v2(), is(notNullValue())); - final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + final String outToken = spnegoClient.handleResponse(future.actionGet().v2()); assertThat(outToken, is(nullValue())); assertThat(spnegoClient.isEstablished(), is(true)); } } + private void unwrapExpectedExceptionFromFutureAndThrow(PlainActionFuture> future) throws Throwable { + try { + future.actionGet(); + } catch (Throwable t) { + Throwable throwThis = t; + while (throwThis instanceof UncategorizedExecutionException || throwThis instanceof ExecutionException) { + throwThis = throwThis.getCause(); + } + throw throwThis; + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java index c82d4e9502a4f..4fce1d1debb48 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java @@ -10,6 +10,7 @@ import com.unboundid.ldap.sdk.SearchResult; import com.unboundid.ldap.sdk.SearchScope; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.env.Environment; @@ -61,13 +62,13 @@ public void testClientServiceMutualAuthentication() throws PrivilegedActionExcep final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); // Handle Authz header which contains base64 token - final Tuple userNameOutToken = - new KerberosTicketValidator().validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, true); - assertThat(userNameOutToken, is(notNullValue())); - assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); + final PlainActionFuture> future = new PlainActionFuture<>(); + new KerberosTicketValidator().validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, true, future); + assertThat(future.actionGet(), is(notNullValue())); + assertThat(future.actionGet().v1(), equalTo(principalName(clientUserName))); // Authenticate service on client side. - final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + final String outToken = spnegoClient.handleResponse(future.actionGet().v2()); assertThat(outToken, is(nullValue())); assertThat(spnegoClient.isEstablished(), is(true)); } From 375954fa7aa9c235bf14030dfbb7f453f811cad8 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Thu, 12 Jul 2018 14:44:40 +1000 Subject: [PATCH 5/9] [Kerberos] Remove realm from principal name (#31928) This commit adds support for removing realm name from the Kerberos principal name. The principal names in Kerberos are in the form primary/instance@realm. Since we will be supporting user lookups and depending on the scenario we may want to remove the REALM part and use the username for lookup or role mapping. This change adds a new setting with the default value false to control removing of realm name. Modified tests to randomly use this setting during testing. --- .../authc/kerberos/KerberosRealmSettings.java | 5 ++- .../authc/kerberos/KerberosRealm.java | 24 +++++++++++- .../KerberosRealmAuthenticateFailedTests.java | 6 ++- .../kerberos/KerberosRealmCacheTests.java | 22 ++++++----- .../kerberos/KerberosRealmSettingsTests.java | 8 +++- .../authc/kerberos/KerberosRealmTestCase.java | 39 ++++++++++++++++++- .../authc/kerberos/KerberosRealmTests.java | 20 ++++++---- .../kerberos/support/KerberosTestCase.java | 8 ++-- 8 files changed, 104 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java index a3dfb501928ee..7524ef08c1e72 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java @@ -27,6 +27,8 @@ public final class KerberosRealmSettings { Setting.simpleString("keytab.path", Property.NodeScope); public static final Setting SETTING_KRB_DEBUG_ENABLE = Setting.boolSetting("krb.debug", Boolean.FALSE, Property.NodeScope); + public static final Setting SETTING_REMOVE_REALM_NAME = + Setting.boolSetting("remove_realm_name", Boolean.FALSE, Property.NodeScope); // Cache private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); @@ -42,6 +44,7 @@ private KerberosRealmSettings() { * @return the valid set of {@link Setting}s for a {@value #TYPE} realm */ public static Set> getSettings() { - return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE); + return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE, + SETTING_REMOVE_REALM_NAME); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java index 81daa07fe7760..20c5d21c192ab 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java @@ -62,6 +62,7 @@ public final class KerberosRealm extends Realm implements CachingRealm { private final ThreadPool threadPool; private final Path keytabPath; private final boolean enableKerberosDebug; + private final boolean removeRealmName; public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, final ThreadPool threadPool) { this(config, nativeRoleMappingStore, new KerberosTicketValidator(), threadPool, null); @@ -88,6 +89,7 @@ public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nati this.threadPool = threadPool; this.keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); this.enableKerberosDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); + this.removeRealmName = KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(config.settings()); } @Override @@ -126,7 +128,8 @@ public void authenticate(final AuthenticationToken token, final ActionListener { if (userPrincipalNameOutToken.v1() != null) { - buildUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener); + final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1()); + buildUser(username, userPrincipalNameOutToken.v2(), listener); } else { /** * This is when security context could not be established may be due to ongoing @@ -145,6 +148,25 @@ public void authenticate(final AuthenticationToken token, final ActionListener handleException(e, listener))); } + /** + * Usually principal names are in the form 'user/instance@REALM'. This method + * removes '@REALM' part from the principal name if + * {@link KerberosRealmSettings#SETTING_REMOVE_REALM_NAME} is {@code true} else + * will return the input string. + * + * @param principalName user principal name + * @return username after removal of realm + */ + protected String maybeRemoveRealmName(final String principalName) { + if (this.removeRealmName) { + int foundAtIndex = principalName.indexOf('@'); + if (foundAtIndex > 0) { + return principalName.substring(0, foundAtIndex); + } + } + return principalName; + } + private void handleException(Exception e, final ActionListener listener) { if (e instanceof LoginException) { listener.onResponse(AuthenticationResult.terminate("failed to authenticate user, service login failure", diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java index 144bec3d58315..7853e18a01b87 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java @@ -42,7 +42,7 @@ public void testAuthenticateWithNonKerberosAuthenticationToken() { } public void testAuthenticateDifferentFailureScenarios() throws LoginException, GSSException { - final String username = randomAlphaOfLength(5); + final String username = randomPrincipalName(); final String outToken = randomAlphaOfLength(10); final KerberosRealm kerberosRealm = createKerberosRealm(username); final boolean validTicket = rarely(); @@ -76,7 +76,9 @@ public void testAuthenticateDifferentFailureScenarios() throws LoginException, G assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE))); } else { if (validTicket) { - final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final String expectedUsername = maybeRemoveRealmName(username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, + true); assertSuccessAuthenticationResult(expectedUser, outToken, result); } else { assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE))); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index 24712e53b504e..c6d114de93b24 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -36,11 +36,12 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { public void testAuthenticateWithCache() throws LoginException, GSSException { - final String username = randomAlphaOfLength(5); + final String username = randomPrincipalName(); final String outToken = randomAlphaOfLength(10); final KerberosRealm kerberosRealm = createKerberosRealm(username); - final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final String expectedUsername = maybeRemoveRealmName(username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); @@ -62,7 +63,7 @@ public void testAuthenticateWithCache() throws LoginException, GSSException { public void testCacheInvalidationScenarios() throws LoginException, GSSException { final String outToken = randomAlphaOfLength(10); - final List userNames = Arrays.asList(randomAlphaOfLength(5) + "@REALM", randomAlphaOfLength(5) + "@REALM"); + final List userNames = Arrays.asList(randomPrincipalName(), randomPrincipalName()); final KerberosRealm kerberosRealm = createKerberosRealm(userNames.toArray(new String[0])); verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); @@ -71,7 +72,8 @@ public void testCacheInvalidationScenarios() throws LoginException, GSSException final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(authNUsername, outToken), null); - final User expectedUser = new User(authNUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final String expectedUsername = maybeRemoveRealmName(authNUsername); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); @@ -81,7 +83,7 @@ public void testCacheInvalidationScenarios() throws LoginException, GSSException if (expireAll) { kerberosRealm.expireAll(); } else { - kerberosRealm.expire(expireThisUser); + kerberosRealm.expire(maybeRemoveRealmName(expireThisUser)); } final User user2 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); @@ -100,14 +102,16 @@ public void testCacheInvalidationScenarios() throws LoginException, GSSException public void testAuthenticateWithValidTicketSucessAuthnWithUserDetailsWhenCacheDisabled() throws LoginException, GSSException, IOException { - final String username = randomAlphaOfLength(5); - final String outToken = randomAlphaOfLength(10); // if cache.ttl <= 0 then the cache is disabled settings = KerberosTestCase.buildKerberosRealmSettings( - KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true); + KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true, + randomBoolean()); + final String username = randomPrincipalName(); + final String outToken = randomAlphaOfLength(10); final KerberosRealm kerberosRealm = createKerberosRealm(username); - final User expectedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final String expectedUsername = maybeRemoveRealmName(username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java index 876ebdc574136..c536566a73f60 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -31,13 +31,17 @@ public void testKerberosRealmSettings() throws IOException { KerberosTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); final Integer maxUsers = randomInt(); final String cacheTTL = randomLongBetween(10L, 100L) + "m"; - final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, true); + final boolean enableDebugLogs = randomBoolean(); + final boolean removeRealmName = randomBoolean(); + final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, enableDebugLogs, + removeRealmName); assertThat(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings), equalTo(keytabPathConfig)); assertThat(KerberosRealmSettings.CACHE_TTL_SETTING.get(settings), equalTo(TimeValue.parseTimeValue(cacheTTL, KerberosRealmSettings.CACHE_TTL_SETTING.getKey()))); assertThat(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(settings), equalTo(maxUsers)); - assertThat(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings), is(true)); + assertThat(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings), is(enableDebugLogs)); + assertThat(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(settings), is(removeRealmName)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index 85e0d88707216..1a0ab149035c6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -20,6 +20,7 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; @@ -33,8 +34,10 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -67,7 +70,8 @@ public void setup() throws Exception { resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); dir = createTempDir(); globalSettings = Settings.builder().put("path.home", dir).build(); - settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString()); + settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), + 100, "10m", true, randomBoolean()); } @After @@ -111,7 +115,8 @@ protected KerberosRealm createKerberosRealm(final String... userForRoleMapping) } @SuppressWarnings("unchecked") - protected NativeRoleMappingStore roleMappingStore(final List expectedUserNames) { + protected NativeRoleMappingStore roleMappingStore(final List userNames) { + final List expectedUserNames = userNames.stream().map(this::maybeRemoveRealmName).collect(Collectors.toList()); final Client mockClient = mock(Client.class); when(mockClient.threadPool()).thenReturn(threadPool); when(mockClient.settings()).thenReturn(settings); @@ -133,4 +138,34 @@ protected NativeRoleMappingStore roleMappingStore(final List expectedUse return roleMapper; } + + protected String randomPrincipalName() { + final StringBuilder principalName = new StringBuilder(); + principalName.append(randomAlphaOfLength(5)); + final boolean withInstance = randomBoolean(); + if (withInstance) { + principalName.append("/").append(randomAlphaOfLength(5)); + } + principalName.append(randomAlphaOfLength(5).toUpperCase(Locale.ROOT)); + return principalName.toString(); + } + + /** + * Usually principal names are in the form 'user/instance@REALM'. This method + * removes '@REALM' part from the principal name if + * {@link KerberosRealmSettings#SETTING_REMOVE_REALM_NAME} is {@code true} else + * will return the input string. + * + * @param principalName user principal name + * @return username after removal of realm + */ + protected String maybeRemoveRealmName(final String principalName) { + if (KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(settings)) { + int foundAtIndex = principalName.indexOf('@'); + if (foundAtIndex > 0) { + return principalName.substring(0, foundAtIndex); + } + } + return principalName; + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java index c504f79233b22..43536abaf29e1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -48,13 +48,14 @@ public void testSupports() { } public void testAuthenticateWithValidTicketSucessAuthnWithUserDetails() throws LoginException, GSSException { - final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); - - final User expectedUser = new User("test@REALM", roles.toArray(new String[roles.size()]), null, null, null, true); + final String username = randomPrincipalName(); + final KerberosRealm kerberosRealm = createKerberosRealm(username); + final String expectedUsername = maybeRemoveRealmName(username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); - mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>("test@REALM", "out-token"), null); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null); final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); final PlainActionFuture future = new PlainActionFuture<>(); @@ -69,7 +70,8 @@ public void testAuthenticateWithValidTicketSucessAuthnWithUserDetails() throws L } public void testFailedAuthorization() throws LoginException, GSSException { - final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + final String username = randomPrincipalName(); + final KerberosRealm kerberosRealm = createKerberosRealm(username); final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); @@ -80,13 +82,15 @@ public void testFailedAuthorization() throws LoginException, GSSException { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); assertThat(e.status(), is(RestStatus.FORBIDDEN)); - assertThat(e.getMessage(), equalTo("Expected UPN '" + Arrays.asList("test@REALM") + "' but was 'does-not-exist@REALM'")); + assertThat(e.getMessage(), equalTo("Expected UPN '" + Arrays.asList(maybeRemoveRealmName(username)) + "' but was '" + + maybeRemoveRealmName("does-not-exist@REALM") + "'")); } public void testLookupUser() { - final KerberosRealm kerberosRealm = createKerberosRealm("test@REALM"); + final String username = randomPrincipalName(); + final KerberosRealm kerberosRealm = createKerberosRealm(username); final PlainActionFuture future = new PlainActionFuture<>(); - kerberosRealm.lookupUser("test@REALM", future); + kerberosRealm.lookupUser(username, future); assertThat(future.actionGet(), is(nullValue())); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java index 865688e2b05f5..4e7b34a9e8b3e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java @@ -196,7 +196,7 @@ public static Path writeKeyTab(final Path keytabPath, final String content) thro * @return {@link Settings} for kerberos realm */ public static Settings buildKerberosRealmSettings(final String keytabPath) { - return buildKerberosRealmSettings(keytabPath, 100, "10m", true); + return buildKerberosRealmSettings(keytabPath, 100, "10m", true, false); } /** @@ -206,14 +206,16 @@ public static Settings buildKerberosRealmSettings(final String keytabPath) { * @param maxUsersInCache max users to be maintained in cache * @param cacheTTL time to live for cached entries * @param enableDebugging for krb5 logs + * @param removeRealmName {@code true} if we want to remove realm name from the username of form 'user@REALM' * @return {@link Settings} for kerberos realm */ public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, - final boolean enableDebugging) { + final boolean enableDebugging, final boolean removeRealmName) { final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) - .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging); + .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging) + .put(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.getKey(), removeRealmName); return builder.build(); } From dd7cdfddad5f56c861643d3e2aa0e6fef595d994 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Wed, 18 Jul 2018 16:31:31 +1000 Subject: [PATCH 6/9] [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 --- .../src/main/resources/provision/addprinc.sh | 22 +- x-pack/qa/kerberos-tests/build.gradle | 126 +++++++ .../kerberos/KerberosAuthenticationIT.java | 152 +++++++++ ...SpnegoHttpClientConfigCallbackHandler.java | 319 ++++++++++++++++++ .../src/test/resources/plugin-security.policy | 4 + 5 files changed, 616 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/KerberosAuthenticationIT.java create mode 100644 x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java create mode 100644 x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy 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..e6d117fb17652 --- /dev/null +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -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 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) +} + +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 +} \ No newline at end of file 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..d5928cb58f687 --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationIT.java @@ -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. + *

+ * 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) () -> { + return restClient.performRequest(request); + }, accessControlContext); + + 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("kerb_test")); + } + } + + private Map 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)); + } + } +} 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 new file mode 100644 index 0000000000000..a9a76b71c8535 --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoHttpClientConfigCallbackHandler.java @@ -0,0 +1,319 @@ +/* + * 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; +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.elasticsearch.ExceptionsHelper; +import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback; +import org.elasticsearch.common.settings.SecureString; +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.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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; + +/** + * 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 configured credentials either password or keytab for authentication. + */ +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(); + + 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 LoginContext loginContext; + + /** + * 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) { + this.userPrincipalName = userPrincipalName; + this.password = password; + this.keytabPath = null; + this.enableDebugLogs = enableDebugLogs; + } + + /** + * 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 SpnegoHttpClientConfigCallbackHandler(final String userPrincipalName, final String keytabPath, final boolean enableDebugLogs) { + this.userPrincipalName = userPrincipalName; + this.keytabPath = keytabPath; + this.password = null; + this.enableDebugLogs = enableDebugLogs; + } + + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + setupSpnegoAuthSchemeSupport(httpClientBuilder); + return httpClientBuilder; + } + + 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 AccessControlContext acc = AccessController.getContext(); + final GSSCredential credential = doAsPrivilegedWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssManager.createCredential(gssUserPrincipalName, + GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.INITIATE_ONLY), + acc); + + 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 (PrivilegedActionException e) { + throw new RuntimeException(e.getCause()); + } + httpClientBuilder.setDefaultAuthSchemeRegistry(authSchemeRegistry); + } + + /** + * If logged in {@link LoginContext} is not available, it attempts login and + * returns {@link LoginContext} + * + * @return {@link LoginContext} + * @throws PrivilegedActionException + */ + public synchronized LoginContext login() throws PrivilegedActionException { + if (this.loginContext == null) { + 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 + * {@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", Boolean.toString(enableDebugLogs)); + 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/resources/plugin-security.policy b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy new file mode 100644 index 0000000000000..fb7936bf62093 --- /dev/null +++ b/x-pack/qa/kerberos-tests/src/test/resources/plugin-security.policy @@ -0,0 +1,4 @@ +grant { + permission javax.security.auth.AuthPermission "doAsPrivileged"; + 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 f0df110263ab13dfe507948054a2ca1b33ce0635 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 20 Jul 2018 08:37:42 +1000 Subject: [PATCH 7/9] [Kerberos] Refactoring and remove configs with defaults (#32152) This commit does some refactoring to remove support package and move classes to kerberos package. That was the only class in that package, so no need for it to be in separate package. Changes done to use default values for jaas configuration options for the ones which we can use defaults. Fix couple of random failures in tests. Modified `refreshKrb5Config` to use default value `false` in KerberosTicketValidator. If the krb5.conf file is modified then we will need to restart JVM as the config will not be refreshed. For testing, `refreshKrb5Config` is set to `true` as we keep changing the kdc port. This is set in SpnegoClient and only for tests. --- .../security/authc/kerberos/KerberosRealm.java | 1 - .../{support => }/KerberosTicketValidator.java | 4 +--- .../KerberosRealmAuthenticateFailedTests.java | 15 +++++++-------- .../KerberosRealmBootstrapCheckTests.java | 1 - .../authc/kerberos/KerberosRealmCacheTests.java | 1 - .../kerberos/KerberosRealmSettingsTests.java | 1 - .../authc/kerberos/KerberosRealmTestCase.java | 5 +---- .../kerberos/{support => }/KerberosTestCase.java | 3 ++- .../KerberosTicketValidatorTests.java | 3 ++- .../{support => }/SimpleKdcLdapServer.java | 2 +- .../{support => }/SimpleKdcLdapServerTests.java | 3 ++- .../kerberos/{support => }/SpnegoClient.java | 11 +++++------ .../SpnegoHttpClientConfigCallbackHandler.java | 2 -- 13 files changed, 21 insertions(+), 31 deletions(-) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/KerberosTicketValidator.java (98%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/KerberosTestCase.java (98%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/KerberosTicketValidatorTests.java (98%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/SimpleKdcLdapServer.java (99%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/SimpleKdcLdapServerTests.java (96%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/{support => }/SpnegoClient.java (97%) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java index 20c5d21c192ab..b4a8b6aabf076 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java @@ -20,7 +20,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; import org.elasticsearch.xpack.security.authc.support.CachingRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java similarity index 98% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java index 3e837bd6377eb..689ba69f78254 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; @@ -262,8 +262,6 @@ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { options.put("useKeyTab", Boolean.TRUE.toString()); options.put("storeKey", Boolean.TRUE.toString()); options.put("doNotPrompt", Boolean.TRUE.toString()); - options.put("renewTGT", Boolean.FALSE.toString()); - options.put("refreshKrb5Config", Boolean.TRUE.toString()); options.put("isInitiator", Boolean.FALSE.toString()); options.put("debug", Boolean.toString(krbDebug)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java index 7853e18a01b87..5bc239241cf11 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java @@ -67,18 +67,17 @@ public void testAuthenticateDifferentFailureScenarios() throws LoginException, G final boolean nullKerberosAuthnToken = rarely(); final KerberosAuthenticationToken kerberosAuthenticationToken = nullKerberosAuthnToken ? null : new KerberosAuthenticationToken(decodedTicket); - - final PlainActionFuture future = new PlainActionFuture<>(); - kerberosRealm.authenticate(kerberosAuthenticationToken, future); - AuthenticationResult result = future.actionGet(); - assertThat(result, is(notNullValue())); if (nullKerberosAuthnToken) { - assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE))); + expectThrows(AssertionError.class, + () -> kerberosRealm.authenticate(kerberosAuthenticationToken, PlainActionFuture.newFuture())); } else { + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + AuthenticationResult result = future.actionGet(); + assertThat(result, is(notNullValue())); if (validTicket) { final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, - true); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); assertSuccessAuthenticationResult(expectedUser, outToken, result); } else { assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE))); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java index d2a40f0f6162f..b6e1df9ddbb79 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmBootstrapCheckTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; import java.io.IOException; import java.nio.file.Path; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index c6d114de93b24..69ebe15c5d74b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; import org.ietf.jgss.GSSException; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java index c536566a73f60..2e47d03d49d06 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; import java.io.IOException; import java.nio.file.Files; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index 1a0ab149035c6..9c2c6484c82ab 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -6,13 +6,12 @@ package org.elasticsearch.xpack.security.authc.kerberos; -import com.google.common.collect.Sets; - import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.Client; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; @@ -23,8 +22,6 @@ import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java similarity index 98% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java index 4e7b34a9e8b3e..891f400c7be60 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; @@ -69,6 +69,7 @@ public abstract class KerberosTestCase extends ESTestCase { unsupportedLocaleLanguages.add("hi"); unsupportedLocaleLanguages.add("uz"); unsupportedLocaleLanguages.add("fa"); + unsupportedLocaleLanguages.add("ks"); } @BeforeClass diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java similarity index 98% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java index 16690ec3cc304..e12b9c5a692c6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; @@ -13,6 +13,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosTicketValidator; import org.ietf.jgss.GSSException; import java.io.IOException; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java similarity index 99% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java index 02bc236b8ec3d..426cacb1a034c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java similarity index 96% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java index 4fce1d1debb48..b1c75d957a7c8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.SearchResult; @@ -17,6 +17,7 @@ import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosTicketValidator; import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils; import org.ietf.jgss.GSSException; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java similarity index 97% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java index 527953f8b2d46..1f883b928bd97 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.authc.kerberos.support; +package org.elasticsearch.xpack.security.authc.kerberos; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; @@ -12,6 +12,7 @@ import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosTicketValidator; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; @@ -148,7 +149,7 @@ public void close() throws LoginException, GSSException, PrivilegedActionExcepti } /** - * @return {@code true} If the context was established + * @return {@code true} If the gss security context was established */ boolean isEstablished() { return gssContext.isEstablished(); @@ -196,12 +197,10 @@ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { final Map options = new HashMap<>(); options.put("principal", principal); options.put("storeKey", Boolean.TRUE.toString()); - options.put("useTicketCache", Boolean.FALSE.toString()); - options.put("useKeyTab", Boolean.FALSE.toString()); - options.put("renewTGT", Boolean.FALSE.toString()); - options.put("refreshKrb5Config", Boolean.TRUE.toString()); options.put("isInitiator", Boolean.TRUE.toString()); options.put("debug", Boolean.TRUE.toString()); + // Refresh Krb5 config during tests as the port keeps changing for kdc server + options.put("refreshKrb5Config", Boolean.TRUE.toString()); 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/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 a9a76b71c8535..e5768d8f2e944 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 @@ -304,10 +304,8 @@ private abstract static class AbstractJaasConf extends Configuration { 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", Boolean.toString(enableDebugLogs)); addOptions(options); return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, From 141cee25cdc99f06f3ce1e255d44c4bb19f5c0c5 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 20 Jul 2018 16:34:53 +1000 Subject: [PATCH 8/9] [Kerberos] Fix to audit log authc_failed event once (#32220) The exception was being sent twice due to incorrect handling of conditional statements causing multiple authentication_failed events in audit logs. --- .../kerberos/KerberosTicketValidator.java | 6 ++--- .../KerberosTicketValidatorTests.java | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java index 689ba69f78254..a63d90178dca4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidator.java @@ -96,11 +96,11 @@ public void validateTicket(final byte[] decodedToken, final Path keytabPath, fin } catch (PrivilegedActionException pve) { if (pve.getCause() instanceof LoginException) { actionListener.onFailure((LoginException) pve.getCause()); - } - if (pve.getCause() instanceof GSSException) { + } else if (pve.getCause() instanceof GSSException) { actionListener.onFailure((GSSException) pve.getCause()); + } else { + actionListener.onFailure(pve.getException()); } - actionListener.onFailure(pve.getException()); } finally { privilegedLogoutNoThrow(loginContext); privilegedDisposeNoThrow(gssContext); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java index e12b9c5a692c6..8f35e0bde4454 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.security.authc.kerberos; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; @@ -13,7 +14,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.elasticsearch.xpack.security.authc.kerberos.KerberosTicketValidator; import org.ietf.jgss.GSSException; import java.io.IOException; @@ -25,6 +25,7 @@ import javax.security.auth.login.LoginException; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -57,10 +58,23 @@ public void testInvalidKerbTicketFailsValidation() throws Exception { final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - final PlainActionFuture> future = new PlainActionFuture<>(); - kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, future); - final GSSException gssException = expectThrows(GSSException.class, () -> unwrapExpectedExceptionFromFutureAndThrow(future)); - assertThat(gssException.getMajor(), equalTo(GSSException.DEFECTIVE_TOKEN)); + kerberosTicketValidator.validateTicket(Base64.getDecoder().decode(base64KerbToken), keytabPath, true, + new ActionListener>() { + boolean exceptionHandled = false; + + @Override + public void onResponse(Tuple response) { + fail("expected exception to be thrown of type GSSException"); + } + + @Override + public void onFailure(Exception e) { + assertThat(exceptionHandled, is(false)); + assertThat(e, instanceOf(GSSException.class)); + assertThat(((GSSException) e).getMajor(), equalTo(GSSException.DEFECTIVE_TOKEN)); + exceptionHandled = true; + } + }); } public void testWhenKeyTabWithInvalidContentFailsValidation() From 158f58557faddf8fae7415579439460f28949c64 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Sat, 21 Jul 2018 03:45:14 +1000 Subject: [PATCH 9/9] [Kerberos] Remove deprecated char ':' from build.gradle (#32247) From 5.0 onwards use of few characters will not be allowed, one of them is ':'. This commit removes that character. Also add dependency for copy task on creation of principal names which caused problems with clean test runs. --- x-pack/qa/kerberos-tests/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index e6d117fb17652..5caf5d6947e8c 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -41,12 +41,12 @@ task krb5AddPrincipals { dependsOn krb5kdcFixture } List principals = [ "HTTP/localhost", "peppa", - "george:dino" + "george~dino" ] String realm = "BUILD.ELASTIC.CO" for (String principal : principals) { - String[] princPwdPair = principal.split(':'); + String[] princPwdPair = principal.split('~'); String princName = princPwdPair[0]; String password = ""; if (princPwdPair.length > 1) { @@ -67,6 +67,7 @@ task copyKeytabToGeneratedResources(type: Copy) { Path peppaKeytab = project(':test:fixtures:krb5kdc-fixture').buildDir.toPath().resolve("keytabs").resolve("peppa.keytab").toAbsolutePath() from peppaKeytab; into generatedResources + dependsOn krb5AddPrincipals } integTestCluster { @@ -123,4 +124,4 @@ if (project.rootProject.vagrantSupported == false) { 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 +}