From b331ab618a7a241a0f06052e1a79d71f353a41ff Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 31 May 2018 16:02:54 +1000 Subject: [PATCH 01/24] Add support framework for Kerberos Realm This commit adds framework to support Kerberos authN in elasticsearch. For Kerberos Realm, KerberosRealmSettings: Captures settings required for Kerberos KerberosAuthenticationToken: Handles extraction of token from request KerberosTicketValidator: Used for kerberos ticket validation and gss context establishment. Changes in plugin-security policy to add required permissions For testing, KerberosTestCase is the base class to start/stop kdc server and build test settings. MiniKdc(Hadoop) is a wrapper around SimpleKdcServer(ApacheDS), simplifies in memory testing with KDC. --- .../authc/kerberos/KerberosRealmSettings.java | 52 +++ x-pack/plugin/security/build.gradle | 65 +++- .../kerberos/KerberosAuthenticationToken.java | 114 ++++++ .../support/KerberosTicketValidator.java | 258 +++++++++++++ .../plugin-metadata/plugin-security.policy | 15 + .../KerberosAuthenticationTokenTests.java | 143 +++++++ .../kerberos/KerberosRealmSettingsTests.java | 41 ++ .../kerberos/support/KerberosTestCase.java | 142 +++++++ .../support/KerberosTicketValidatorTests.java | 155 ++++++++ .../authc/kerberos/support/MiniKdc.java | 352 ++++++++++++++++++ .../authc/kerberos/support/MiniKdcTests.java | 80 ++++ .../authc/kerberos/support/SpnegoClient.java | 190 ++++++++++ .../src/test/resources/minikdc-krb5.conf | 25 ++ .../security/src/test/resources/minikdc.ldiff | 40 ++ 14 files changed, 1671 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/MiniKdc.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.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/minikdc-krb5.conf create mode 100644 x-pack/plugin/security/src/test/resources/minikdc.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..3fac552ccaa58 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java @@ -0,0 +1,52 @@ +/* + * 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 java.util.HashSet; +import java.util.Set; + +/** + * Kerberos Realm settings + */ +public final class KerberosRealmSettings { + public static final String TYPE = "kerberos"; + + /** + * Kerberos Key tab for Elasticsearch HTTP Service and Kibana HTTP Service
+ * Uses single key tab for multiple service accounts. + */ + public static final Setting HTTP_SERVICE_KEYTAB_PATH = + Setting.simpleString("http.service.keytab.path", Setting.Property.NodeScope); + public static final Setting SETTING_KRB_DEBUG_ENABLE = + Setting.boolSetting("krb.debug", Boolean.FALSE, Setting.Property.Dynamic, Property.NodeScope); + // Cache + private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); + public static final Setting CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); + private static final int DEFAULT_MAX_USERS = 100_000; // 100k users + public static final Setting CACHE_MAX_USERS_SETTING = + Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Setting.Property.NodeScope); + + private KerberosRealmSettings() { + /* Empty private constructor */ + } + + /** + * @return Set of {@link Setting}s for {@value #TYPE} + */ + public static Set> getSettings() { + Set> settings = new HashSet<>(); + settings.add(HTTP_SERVICE_KEYTAB_PATH); + settings.add(CACHE_TTL_SETTING); + settings.add(CACHE_MAX_USERS_SETTING); + settings.add(SETTING_KRB_DEBUG_ENABLE); + return settings; + } +} diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 12533a389b5f1..071e79f42c206 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.0.1') + testCompile('org.apache.kerby:kerb-client:1.0.1') + testCompile('org.apache.kerby:kerby-config:1.0.1') + testCompile('org.apache.kerby:kerb-core:1.0.1') + testCompile('org.apache.kerby:kerby-pkix:1.0.1') + testCompile('org.apache.kerby:kerby-asn1:1.0.1') + testCompile('org.apache.kerby:kerby-util:1.0.1') + testCompile('org.apache.kerby:kerb-common:1.0.1') + testCompile('org.apache.kerby:kerb-crypto:1.0.1') + testCompile('org.apache.kerby:kerb-util:1.0.1') + testCompile('org.apache.kerby:token-provider:1.0.1') + testCompile('com.nimbusds:nimbus-jose-jwt:3.10') + testCompile('net.jcip:jcip-annotations:1.0') + testCompile('org.apache.kerby:kerb-admin:1.0.1') + testCompile('org.apache.kerby:kerb-server:1.0.1') + testCompile('org.apache.kerby:kerb-identity:1.0.1') + testCompile('org.apache.kerby:kerby-xdr:1.0.1') + + // LDAP backend support for SimpleKdcServer + testCompile('org.apache.kerby:kerby-backend:1.0.1') + testCompile('org.apache.kerby:ldap-backend:1.0.1') + testCompile('org.apache.kerby:kerb-identity:1.0.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.16') + testCompile('org.apache.directory.api:api-util:1.0.0') + testCompile('org.apache.directory.api:api-i18n:1.0.0') + testCompile('org.apache.directory.api:api-ldap-model:1.0.0') + testCompile('org.apache.directory.api:api-asn1-api:1.0.0') + testCompile('org.apache.directory.api:api-asn1-ber:1.0.0') + 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" @@ -87,6 +148,8 @@ dependencyLicenses { licenseHeaders { // This class was sourced from apache directory studio for some microsoft-specific logic excludes << 'org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySIDUtil.java' + // This class was sourced from Hadoop as a wrapper around Apache DS, used only for tests + excludes << 'org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java' } forbiddenPatterns { 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..f1e22f7569d35 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationToken.java @@ -0,0 +1,114 @@ +/* + * 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.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +/** + * Holds on to base 64 encoded ticket, also helps extracting token from + * {@link ThreadContext} + */ +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_AUTH_HEADER = "Negotiate "; + public static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; + + private final String principalName; + private String base64EncodedTicket; + + public KerberosAuthenticationToken(final String base64EncodedToken) { + this.principalName = UNAUTHENTICATED_PRINCIPAL_NAME; + this.base64EncodedTicket = base64EncodedToken; + } + + /** + * Extract token from header and if valid {@link #NEGOTIATE_AUTH_HEADER} then + * returns {@link KerberosAuthenticationToken} + * + * @param context {@link ThreadContext} + * @return returns {@code null} if {@link #AUTH_HEADER} is empty or not an + * {@link #NEGOTIATE_AUTH_HEADER} else returns valid + * {@link KerberosAuthenticationToken} + */ + public static KerberosAuthenticationToken extractToken(final ThreadContext context) { + final String authHeader = context.getHeader(AUTH_HEADER); + if (Strings.isNullOrEmpty(authHeader)) { + return null; + } + + if (authHeader.startsWith(NEGOTIATE_AUTH_HEADER) == Boolean.FALSE) { + return null; + } + + final String base64EncodedToken = authHeader.substring(NEGOTIATE_AUTH_HEADER.length()).trim(); + if (Strings.isEmpty(base64EncodedToken)) { + throw unauthorized("invalid negotiate authentication header value, expected base64 encoded token but value is empty", null); + } + final byte[] base64Token = base64EncodedToken.getBytes(StandardCharsets.UTF_8); + byte[] decodedKerberosTicket = null; + IllegalArgumentException rootCause = null; + try { + decodedKerberosTicket = Base64.getDecoder().decode(base64Token); + } catch (IllegalArgumentException iae) { + rootCause = iae; + } + + if (decodedKerberosTicket == null || decodedKerberosTicket.length == 0) { + throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", rootCause, + base64EncodedToken); + } + return new KerberosAuthenticationToken(base64EncodedToken); + } + + @Override + public String principal() { + return principalName; + } + + @Override + public Object credentials() { + return base64EncodedTicket; + } + + @Override + public void clearCredentials() { + this.base64EncodedTicket = null; + } + + @Override + public int hashCode() { + return Objects.hash(principalName, base64EncodedTicket); + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null) + return false; + if (getClass() != other.getClass()) + return false; + final KerberosAuthenticationToken otherKerbToken = (KerberosAuthenticationToken) other; + return Objects.equals(otherKerbToken.principal(), principal()) && Objects.equals(otherKerbToken.credentials(), credentials()); + } + + private static ElasticsearchSecurityException unauthorized(String message, Throwable cause, Object... args) { + ElasticsearchSecurityException ese = new ElasticsearchSecurityException(message, RestStatus.UNAUTHORIZED, args); + ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER); + 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..726b47938da93 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidator.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.Level; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.ESLoggerFactory; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +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 org.ietf.jgss.Oid; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +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; + +/** + * Responsible for validating Kerberos ticket
+ * Performs service login using keytab, supports multiple principals in keytab. + */ +public class KerberosTicketValidator { + public 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) { + 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. + * + * @param servicePrincipalName Service principal name + * @param nameType {@link GSSName} type depending on GSS API principal entity + * @param base64Ticket base64 encoded kerberos ticket + * @param config {@link RealmConfig} + * @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 String servicePrincipalName, Oid nameType, final String base64Ticket, + final RealmConfig config) throws LoginException, GSSException { + final GSSManager gssManager = GSSManager.getInstance(); + GSSContext gssContext = null; + LoginContext loginContext = null; + try { + Path keyTabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); + if (Files.exists(keyTabPath) == false) { + throw new IllegalArgumentException("configured service key tab file does not exist for " + + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH)); + } + // do service login + loginContext = serviceLogin(servicePrincipalName, keyTabPath.toString(), config.settings()); + // create credentials + GSSCredential serviceCreds = createCredentials(servicePrincipalName, nameType, gssManager, loginContext); + // create gss context + gssContext = gssManager.createContext(serviceCreds); + final byte[] outToken = acceptSecContext(base64Ticket, gssContext, loginContext); + + String base64OutToken = null; + if (outToken != null && outToken.length > 0) { + base64OutToken = Base64.getEncoder().encodeToString(outToken); + } + + return new Tuple<>(gssContext.isEstablished() ? gssContext.getSrcName().toString() : null, base64OutToken); + } catch (PrivilegedActionException pve) { + if (pve.getException() instanceof LoginException) { + throw (LoginException) pve.getCause(); + } + if (pve.getException() instanceof GSSException) { + throw (GSSException) pve.getCause(); + } + throw ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(pve)); + } finally { + privilegedDisposeNoThrow(loginContext); + privilegedCloseNoThrow(gssContext); + } + } + + private static byte[] acceptSecContext(final String base64Ticket, GSSContext gssContext, LoginContext loginContext) + throws PrivilegedActionException { + final GSSContext finalGSSContext = gssContext; + final byte[] token = Base64.getDecoder().decode(base64Ticket); + // process token with gss context + return doAsWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> finalGSSContext.acceptSecContext(token, 0, token.length)); + } + + private static GSSCredential createCredentials(final String servicePrincipalName, Oid nameType, final GSSManager gssManager, + LoginContext loginContext) throws GSSException, PrivilegedActionException { + final GSSName gssServicePrincipalName; + if (servicePrincipalName.equals("*") == false) { + gssServicePrincipalName = gssManager.createName(servicePrincipalName, nameType); + } else { + gssServicePrincipalName = null; + } + return doAsWrapper(loginContext.getSubject(), (PrivilegedExceptionAction) () -> gssManager + .createCredential(gssServicePrincipalName, GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.ACCEPT_ONLY)); + } + + /** + * Privileged Wrapper that invokes action with Subject.doAs + * + * @param subject {@link Subject} + * @param action {@link PrivilegedExceptionAction} action for performing inside + * Subject.doAs + * @return + * @throws PrivilegedActionException + */ + private static T doAsWrapper(Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); + } catch (PrivilegedActionException pae) { + throw (PrivilegedActionException) pae.getException(); + } + } + + /** + * Privileged wrapper for closing GSSContext, does not throw exceptions but logs + * them as warning. + * + * @param gssContext + */ + private static void privilegedCloseNoThrow(final GSSContext gssContext) { + if (gssContext != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + gssContext.dispose(); + return null; + }); + } catch (PrivilegedActionException e) { + RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); + LOGGER.log(Level.DEBUG, "Could not dispose GSS Context", rte); + } + return; + } + } + + /** + * Privileged wrapper for closing LoginContext, does not throw exceptions but + * logs them as warning. + * + * @param loginContext + */ + private static void privilegedDisposeNoThrow(final LoginContext loginContext) { + if (loginContext != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + loginContext.logout(); + return null; + }); + } catch (PrivilegedActionException e) { + RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); + LOGGER.log(Level.DEBUG, "Could not close LoginContext", rte); + } + } + } + + /** + * Performs authentication using provided principal name and keytab + * + * @param principal Principal name + * @param keytabFilePath Keytab file path + * @param settings {@link Settings} + * @return authenticated {@link LoginContext} instance. Note: This needs to be + * closed {@link LoginContext#logout()} after usage. + * @throws PrivilegedActionException + */ + private static LoginContext serviceLogin(final String principal, final String keytabFilePath, final Settings settings) + throws PrivilegedActionException { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + final Set principals = new HashSet<>(); + if (principal.equals("*") == false) { + principals.add(new KerberosPrincipal(principal)); + } + + final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + + final Configuration conf = new KeytabJaasConf(principal, keytabFilePath, settings); + final LoginContext loginContext = new LoginContext(KEY_TAB_CONF_NAME, subject, null, conf); + loginContext.login(); + return loginContext; + }); + } + + /** + * Instead of jaas.conf, this requires refresh of {@link Configuration}. + */ + static class KeytabJaasConf extends Configuration { + private final String principal; + private final String keytabFilePath; + private final Settings settings; + + KeytabJaasConf(final String principal, final String keytabFilePath, final Settings settings) { + this.principal = principal; + this.keytabFilePath = keytabFilePath; + this.settings = settings; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final Map options = new HashMap<>(); + options.put("keyTab", keytabFilePath); + options.put("principal", 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", KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings).toString()); + + return new AppConfigurationEntry[] { + new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, 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..e4f9bb743f86b --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosAuthenticationTokenTests.java @@ -0,0 +1,143 @@ +/* + * 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.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; + +public class KerberosAuthenticationTokenTests extends ESTestCase { + private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; + + private ThreadContext threadContext; + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() throws IOException { + final Path dir = createTempDir(); + final Path ktab = KerberosTestCase.writeKeyTab(dir, "http.keytab", null); + final Settings settings = KerberosTestCase.buildKerberosRealmSettings(ktab.toString()); + threadContext = new ThreadContext(settings); + } + + @After + public void cleanup() throws IOException { + threadContext.close(); + threadContext = null; + } + + public void testExtractTokenForValidAuthorizationHeader() throws IOException { + final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); + + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + assertNotNull(kerbAuthnToken); + assertEquals(UNAUTHENTICATED_PRINCIPAL_NAME, kerbAuthnToken.principal()); + assertEquals(base64Token, kerbAuthnToken.credentials()); + } + + public void testExtractTokenForInvalidAuthorizationHeaderThrowsException() throws IOException { + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER); + + thrown.expect(ElasticsearchSecurityException.class); + thrown.expectMessage( + Matchers.equalTo("invalid negotiate authentication header value, expected base64 encoded token but value is empty")); + thrown.expect(new BaseMatcher() { + + @Override + public boolean matches(Object item) { + if (item instanceof ElasticsearchSecurityException) { + List authHeaderValue = + ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); + if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.equals(authHeaderValue.get(0))) { + return true; + } + } + return false; + } + + @Override + public void describeTo(Description description) { + } + }); + KerberosAuthenticationToken.extractToken(threadContext); + fail("Expected exception not thrown"); + } + + public void testExtractTokenForNotBase64EncodedTokenThrowsException() throws IOException { + final String notBase64Token = "[B@6499375d"; + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, + KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + notBase64Token); + + thrown.expect(ElasticsearchSecurityException.class); + thrown.expectMessage( + Matchers.equalTo("invalid negotiate authentication header value, could not decode base64 token " + notBase64Token)); + thrown.expect(new BaseMatcher() { + + @Override + public boolean matches(Object item) { + if (item instanceof ElasticsearchSecurityException) { + List authHeaderValue = + ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); + if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.equals(authHeaderValue.get(0))) { + return true; + } + } + return false; + } + + @Override + public void describeTo(Description description) { + } + }); + KerberosAuthenticationToken.extractToken(threadContext); + fail("Expected exception not thrown"); + } + + public void testExtractTokenForNoAuthorizationHeaderShouldReturnNull() throws IOException { + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + assertNull(kerbAuthnToken); + } + + public void testExtractTokenForBasicAuthorizationHeaderShouldReturnNull() throws IOException { + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, "Basic "); + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + assertNull(kerbAuthnToken); + } + + public void testKerberoAuthenticationTokenClearCredentials() { + final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + kerbAuthnToken.clearCredentials(); + assertNull(kerbAuthnToken.credentials()); + } + + public void testEqualsHashCode() { + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken("base64EncodedToken"); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { + return new KerberosAuthenticationToken((String) original.credentials()); + }); + } +} 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..b0a6fea449165 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -0,0 +1,41 @@ +/* + * 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; + +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); + } + KerberosTestCase.writeKeyTab(dir, "config" + dir.getFileSystem().getSeparator() + "http.keytab", null); + final Integer maxUsers = randomInt(); + final String cacheTTL = randomLongBetween(10L, 100L) + "m"; + final Settings settings = KerberosTestCase.buildKerberosRealmSettings("config" + dir.getFileSystem().getSeparator() + "http.keytab", + maxUsers, cacheTTL, true); + + assertEquals("config" + dir.getFileSystem().getSeparator() + "http.keytab", + KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + assertEquals(TimeValue.parseTimeValue(cacheTTL, KerberosRealmSettings.CACHE_TTL_SETTING.getKey()), + KerberosRealmSettings.CACHE_TTL_SETTING.get(settings)); + assertEquals(maxUsers, KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(settings)); + assertEquals(Boolean.TRUE, KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings)); + } + +} 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..c5a1cdea54fc6 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java @@ -0,0 +1,142 @@ +/* + * 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.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +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.Before; + +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 javax.security.auth.Subject; + +/** + * Base Test class for Kerberos + */ +public abstract class KerberosTestCase extends ESTestCase { + + protected MiniKdc miniKdc; + protected Path miniKdcWorkDir = null; + protected InMemoryDirectoryServer ldapServer; + + @Before + public void startMiniKdc() throws Exception { + createLdapService(); + createMiniKdc(); + AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + public Void run() throws Exception { + miniKdc.start(); + return null; + } + }); + } + + @SuppressForbidden(reason = "Test dependency MiniKdc requires java.io.File and needs access to private field") + private void createMiniKdc() throws Exception { + miniKdcWorkDir = createTempDir(); + String backendConf = "kdc_identity_backend = org.apache.kerby.kerberos.kdc.identitybackend.LdapIdentityBackend\n" + + "host=127.0.0.1\n" + "port=" + ldapServer.getListenPort() + "\n" + "admin_dn=uid=admin,ou=system,dc=example,dc=com\n" + + "admin_pw=secret\n" + "base_dn=dc=example,dc=com"; + Files.write(miniKdcWorkDir.resolve("backend.conf"), backendConf.getBytes(StandardCharsets.UTF_8)); + assertTrue(Files.exists(miniKdcWorkDir.resolve("backend.conf"))); + miniKdc = new MiniKdc(MiniKdc.createConf(), miniKdcWorkDir.toFile()); + } + + private void createLdapService() throws Exception { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com"); + config.setSchema(null); + ldapServer = new InMemoryDirectoryServer(config); + ldapServer.importFromLDIF(true, getDataPath("/minikdc.ldiff").toString()); + // Must have privileged access because underlying server will accept socket + // connections + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + ldapServer.startListening(); + return null; + }); + } + + @After + public void tearDownMiniKdc() throws IOException, PrivilegedActionException { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + public Void run() throws Exception { + miniKdc.stop(); + return null; + } + }); + + ldapServer.shutDown(true); + } + + @SuppressForbidden(reason = "Test dependency MiniKdc requires java.io.File") + protected Path createPrincipalKeyTab(final Path dir, final String... principalNames) throws Exception { + final Path ktabPath = dir.resolve(randomAlphaOfLength(10) + ".keytab"); + miniKdc.createPrincipal(ktabPath.toFile(), principalNames); + assertTrue(Files.exists(ktabPath)); + return ktabPath; + } + + protected void createPrincipal(final String principalName, final char[] password) throws Exception { + miniKdc.createPrincipal(principalName, new String(password)); + } + + protected String principalName(final String user) { + return user + "@" + miniKdc.getRealm(); + } + + /** + * Invokes Subject.doAs inside a doPrivileged block + * + * @param subject {@link Subject} + * @param action {@link PrivilegedExceptionAction} action for performing inside + * Subject.doAs + * @return + * @throws PrivilegedActionException + */ + public static T doAsWrapper(Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); + } + + public static Path writeKeyTab(final Path dir, final String name, final String content) throws IOException { + final Path path = dir.resolve(name); + try (BufferedWriter bufferedWriter = Files.newBufferedWriter(path, StandardCharsets.US_ASCII)) { + bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); + } + return path; + } + + public static Settings buildKerberosRealmSettings(final String keytabPath) { + return buildKerberosRealmSettings(keytabPath, 100, "10m", true); + } + + public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, + final boolean enableDebugging) { + 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..75d142a79c107 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java @@ -0,0 +1,155 @@ +/* + * 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.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matchers; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSName; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import javax.security.auth.login.LoginException; + +public class KerberosTicketValidatorTests extends KerberosTestCase { + + private Settings settings; + private List serviceUserNames = new ArrayList<>(); + private String clientUserName; + private Settings globalSettings; + private Path dir; + private KerberosTicketValidator kerberosTicketValidator = new KerberosTicketValidator(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() throws Exception { + dir = createTempDir(); + globalSettings = Settings.builder().put("path.home", dir).build(); + serviceUserNames.clear(); + serviceUserNames.add("HTTP/" + randomAlphaOfLength(10)); + serviceUserNames.add("HTTP/" + randomAlphaOfLength(10)); + Path ktabPathForService = createPrincipalKeyTab(dir, serviceUserNames.toArray(new String[0])); + settings = buildKerberosRealmSettings(ktabPathForService.toString()); + clientUserName = "client-" + randomAlphaOfLength(5); + createPrincipal(clientUserName, "pwd".toCharArray()); + } + + public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Exception { + createPrincipalKeyTab(dir, "differentServer"); + + // Client login and init token preparation + final SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer")); + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + } + + public void testInvalidKerbTicketFailsValidation() throws Exception { + final String base64KerbToken = Base64.getEncoder().encodeToString(randomByteArrayOfLength(5)); + + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + thrown.expect(new GSSExceptionMatcher(GSSException.DEFECTIVE_TOKEN)); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + } + + public void testWhenKeyTabDoesNotExistFailsValidation() throws LoginException, GSSException { + final String base64KerbToken = Base64.getEncoder().encodeToString(randomByteArrayOfLength(5)); + settings = buildKerberosRealmSettings("random-non-existing.keytab".toString()); + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage(Matchers.equalTo("configured service key tab file does not exist for " + + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH))); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + } + + public void testWhenKeyTabWithInvalidContentFailsValidation() + throws LoginException, GSSException, IOException, PrivilegedActionException { + // Client login and init token preparation + final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames))); + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + final Path ktabPath = writeKeyTab(dir, "invalid.keytab", "not - a - valid - key - tab"); + settings = buildKerberosRealmSettings(ktabPath.toString()); + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + } + + public void testValidKebrerosTicket() throws PrivilegedActionException, GSSException, LoginException { + // Client login and init token preparation + final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames))); + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + final Tuple userNameOutToken = + kerberosTicketValidator.validateTicket("*", GSSName.NT_HOSTBASED_SERVICE, base64KerbToken, config); + assertNotNull(userNameOutToken); + assertEquals(principalName(clientUserName), userNameOutToken.v1()); + assertNotNull(userNameOutToken.v2()); + + spnegoClient.handleResponse(userNameOutToken.v2()); + assertTrue(spnegoClient.isEstablished()); + spnegoClient.close(); + } + + class GSSExceptionMatcher extends BaseMatcher { + private int expectedErrorCode; + + GSSExceptionMatcher(int expectedErrorCode) { + this.expectedErrorCode = expectedErrorCode; + } + + @Override + public boolean matches(Object item) { + if (item instanceof GSSException) { + GSSException gssException = (GSSException) item; + if (gssException.getMajor() == expectedErrorCode) { + if (gssException.getMajorString().equals(new GSSException(expectedErrorCode).getMajorString())) { + return true; + } + } + } + return false; + } + + @Override + public void describeTo(Description description) { + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java new file mode 100644 index 0000000000000..6d864fa96fc4c --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java @@ -0,0 +1,352 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.elasticsearch.xpack.security.authc.kerberos.support; + +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.IOUtil; +import org.apache.kerby.util.NetworkUtil; +import org.elasticsearch.common.SuppressForbidden; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Mini KDC based on Apache Directory Server that can be embedded in testcases + * or used from command line as a standalone KDC. + *

+ * From within testcases: + *

+ * MiniKdc sets one System property when started and un-set when stopped: + *

    + *
  • sun.security.krb5.debug: set to the debug value provided in the + * configuration
  • + *
+ * Because of this, multiple MiniKdc instances cannot be started in parallel. + * For example, running testcases in parallel that start a KDC each. To + * accomplish this a single MiniKdc should be used for all testcases running in + * parallel. + *

+ * MiniKdc default configuration values are: + *

    + *
  • org.name=EXAMPLE (used to create the REALM)
  • + *
  • org.domain=COM (used to create the REALM)
  • + *
  • kdc.bind.address=localhost
  • + *
  • kdc.port=0 (ephemeral port)
  • + *
  • instance=DefaultKrbServer
  • + *
  • max.ticket.lifetime=86400000 (1 day)
  • + *
  • max.renewable.lifetime=604800000 (7 days)
  • + *
  • transport=TCP
  • + *
  • debug=false
  • + *
+ * The generated krb5.conf forces TCP connections. + */ +@SuppressForbidden(reason = "Uses apache simple kdc server requires does not yet support java.nio.file") +public class MiniKdc { + + public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf"; + public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug"; + private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class); + + public static final String ORG_NAME = "org.name"; + public static final String ORG_DOMAIN = "org.domain"; + public static final String KDC_BIND_ADDRESS = "kdc.bind.address"; + public static final String KDC_PORT = "kdc.port"; + public static final String INSTANCE = "instance"; + public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime"; + public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime"; + public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime"; + public static final String TRANSPORT = "transport"; + public static final String DEBUG = "debug"; + + private static final Set PROPERTIES = new HashSet(); + private static final Properties DEFAULT_CONFIG = new Properties(); + + static { + PROPERTIES.add(ORG_NAME); + PROPERTIES.add(ORG_DOMAIN); + PROPERTIES.add(KDC_BIND_ADDRESS); + PROPERTIES.add(KDC_BIND_ADDRESS); + PROPERTIES.add(KDC_PORT); + PROPERTIES.add(INSTANCE); + PROPERTIES.add(TRANSPORT); + PROPERTIES.add(MAX_TICKET_LIFETIME); + PROPERTIES.add(MAX_RENEWABLE_LIFETIME); + + DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost"); + DEFAULT_CONFIG.setProperty(KDC_PORT, "0"); + DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer"); + DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE"); + DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM"); + DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP"); + DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000"); + DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000"); + DEFAULT_CONFIG.setProperty(DEBUG, "false"); + } + + /** + * Convenience method that returns MiniKdc default configuration. + *

+ * The returned configuration is a copy, it can be customized before using it to + * create a MiniKdc. + * + * @return a MiniKdc default configuration. + */ + public static Properties createConf() { + return (Properties) DEFAULT_CONFIG.clone(); + } + + private Properties conf; + private SimpleKdcServer simpleKdc; + private int port; + private String realm; + private File workDir; + private File krb5conf; + private String transport; + private boolean krb5Debug; + + public void setTransport(String transport) { + this.transport = transport; + } + + /** + * Creates a MiniKdc. + * + * @param conf MiniKdc configuration. + * @param workDir working directory, it should be the build directory. Under + * this directory an ApacheDS working directory will be created, this + * directory will be deleted when the MiniKdc stops. + * @throws Exception thrown if the MiniKdc could not be created. + */ + public MiniKdc(Properties conf, File workDir) throws Exception { + if (!conf.keySet().containsAll(PROPERTIES)) { + Set missingProperties = new HashSet(PROPERTIES); + missingProperties.removeAll(conf.keySet()); + throw new IllegalArgumentException("Missing configuration properties: " + missingProperties); + } + this.workDir = new File(workDir, Long.toString(System.currentTimeMillis())); + if (!this.workDir.exists() && !this.workDir.mkdirs()) { + throw new RuntimeException("Cannot create directory " + this.workDir); + } + LOG.info("Configuration:"); + LOG.info("---------------------------------------------------------------"); + for (Map.Entry entry : conf.entrySet()) { + LOG.info(" {}: {}", entry.getKey(), entry.getValue()); + } + LOG.info("---------------------------------------------------------------"); + this.conf = conf; + port = Integer.parseInt(conf.getProperty(KDC_PORT)); + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH); + } + + /** + * Returns the port of the MiniKdc. + * + * @return the port of the MiniKdc. + */ + public int getPort() { + return port; + } + + /** + * Returns the host of the MiniKdc. + * + * @return the host of the MiniKdc. + */ + public String getHost() { + return conf.getProperty(KDC_BIND_ADDRESS); + } + + /** + * Returns the realm of the MiniKdc. + * + * @return the realm of the MiniKdc. + */ + public String getRealm() { + return realm; + } + + public File getKrb5conf() { + krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF)); + return krb5conf; + } + + /** + * Starts the MiniKdc. + * + * @throws Exception thrown if the MiniKdc could not be started. + */ + public synchronized void start() throws Exception { + if (simpleKdc != null) { + throw new RuntimeException("Already started"); + } + simpleKdc = new SimpleKdcServer(this.workDir.getParentFile(), new KrbConfig()); + simpleKdc.setKdcRealm("EXAMPLE.COM"); + simpleKdc.setKdcHost("localhost"); + simpleKdc.setKdcPort(NetworkUtil.getServerPort()); + + prepareKdcServer(); + simpleKdc.init(); + resetDefaultRealm(); + simpleKdc.start(); + LOG.info("MiniKdc started."); + } + + private void resetDefaultRealm() throws IOException { + InputStream templateResource = new FileInputStream(getKrb5conf().getAbsolutePath()); + String content = IOUtil.readInput(templateResource); + content = content.replaceAll("default_realm = .*\n", "default_realm = " + getRealm() + "\n"); + IOUtil.writeFile(content, getKrb5conf()); + } + + private void prepareKdcServer() throws Exception { + // transport + simpleKdc.setWorkDir(workDir); + simpleKdc.setKdcHost(getHost()); + simpleKdc.setKdcRealm(realm); + if (transport == null) { + transport = conf.getProperty(TRANSPORT); + } + if (port == 0) { + port = NetworkUtil.getServerPort(); + } + if (transport != null) { + if (transport.trim().equals("TCP")) { + simpleKdc.setKdcTcpPort(port); + simpleKdc.setAllowUdp(false); + } else if (transport.trim().equals("UDP")) { + simpleKdc.setKdcUdpPort(port); + simpleKdc.setAllowTcp(false); + } else { + throw new IllegalArgumentException("Invalid transport: " + transport); + } + } else { + throw new IllegalArgumentException("Need to set transport!"); + } + simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, conf.getProperty(INSTANCE)); + if (conf.getProperty(DEBUG) != null) { + krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG)); + } + if (conf.getProperty(MIN_TICKET_LIFETIME) != null) { + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, Long.parseLong(conf.getProperty(MIN_TICKET_LIFETIME))); + } + if (conf.getProperty(MAX_TICKET_LIFETIME) != null) { + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, + Long.parseLong(conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME))); + } + } + + /** + * Stops the MiniKdc + */ + public synchronized void stop() { + if (simpleKdc != null) { + try { + simpleKdc.stop(); + } catch (KrbException e) { + e.printStackTrace(); + } finally { + if (conf.getProperty(DEBUG) != null) { + System.setProperty(SUN_SECURITY_KRB5_DEBUG, Boolean.toString(krb5Debug)); + } + } + } + delete(workDir); + try { + // Will be fixed in next Kerby version. + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + LOG.info("MiniKdc stopped."); + } + + private void delete(File f) { + if (f.isFile()) { + if (!f.delete()) { + LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath()); + } + } else { + File[] fileList = f.listFiles(); + if (fileList != null) { + for (File c : fileList) { + delete(c); + } + } + if (!f.delete()) { + LOG.warn("WARNING: cannot delete directory " + f.getAbsolutePath()); + } + } + } + + /** + * 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(String principal, 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. + * @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. + */ + public synchronized void createPrincipal(File keytabFile, String... principals) throws Exception { + simpleKdc.createPrincipals(principals); + if (keytabFile.exists() && !keytabFile.delete()) { + LOG.error("Failed to delete keytab file: " + keytabFile); + } + for (String principal : principals) { + simpleKdc.getKadmin().exportKeytab(keytabFile, principal); + } + } + + /** + * Set the System property; return the old value for caching. + * + * @param sysprop property + * @param debug true or false + * @return the previous value + */ + private boolean getAndSet(String sysprop, String debug) { + boolean old = Boolean.getBoolean(sysprop); + System.setProperty(sysprop, debug); + return old; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java new file mode 100644 index 0000000000000..519535fbc2ff8 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java @@ -0,0 +1,80 @@ +/* + * 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.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; +import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSName; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.PrivilegedActionException; + +import javax.security.auth.login.LoginException; + +/** + * Tests MiniKdc and framework around it. + */ +public class MiniKdcTests extends KerberosTestCase { + + private Settings settings; + private String serviceUserName; + private String clientUserName; + private Settings globalSettings; + private Path dir; + + @Before + public void setup() throws Exception { + dir = createTempDir(); + globalSettings = Settings.builder().put("path.home", dir).build(); + serviceUserName = "HTTP/" + randomAlphaOfLength(10); + Path ktabPathForService = createPrincipalKeyTab(dir, serviceUserName); + settings = buildKerberosRealmSettings(ktabPathForService.toString()); + clientUserName = "client-" + randomAlphaOfLength(10); + createPrincipal(clientUserName, "pwd".toCharArray()); + } + + @After + public void cleanup() throws IOException { + } + + public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException { + // Client login and init token preparation + final SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName)); + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(base64KerbToken); + + // Service Login + final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); + // Handle Authz header which contains base64 token + final Tuple userNameOutToken = new KerberosTicketValidator().validateTicket(principalName(serviceUserName), + GSSName.NT_USER_NAME, (String) kerbAuthnToken.credentials(), config); + assertNotNull(userNameOutToken); + assertEquals(principalName(clientUserName), userNameOutToken.v1()); + + // Authenticate service on client side. + final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + assertNull(outToken); + assertTrue(spnegoClient.isEstablished()); + + // Close + spnegoClient.close(); + } +} 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..3c88d1041b85c --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SpnegoClient.java @@ -0,0 +1,190 @@ +/* + * 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.ExceptionsHelper; +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 org.ietf.jgss.Oid; + +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.HashMap; +import java.util.HashSet; +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 and handles spnego interactions.
+ * Not thread safe + */ +public class SpnegoClient { + public 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) { + ExceptionsHelper.convertToRuntime(gsse); + } + return oid; + } + + 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; + private boolean isEstablished; + + public SpnegoClient(final String userPrincipalName, final SecureString password, final String servicePrincipalName) + throws PrivilegedActionException, GSSException { + 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, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); + gssContext = gssManager.createContext(gssServicePrincipalName.canonicalize(SPNEGO_OID), SPNEGO_OID, userCreds, + GSSCredential.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + isEstablished = gssContext.isEstablished(); + } + + public String getBase64TicketForSpnegoHeader() throws PrivilegedActionException { + final byte[] outToken = KerberosTestCase.doAsWrapper(loginContext.getSubject(), + (PrivilegedExceptionAction) () -> gssContext.initSecContext(new byte[0], 0, 0)); + return Base64.getEncoder().encodeToString(outToken); + } + + public String handleResponse(final String base64Token) throws PrivilegedActionException { + if (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)); + isEstablished = gssContext.isEstablished(); + if (outToken == null || outToken.length == 0) { + return null; + } + return Base64.getEncoder().encodeToString(outToken); + } + + 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; + }); + } + } + + public boolean isEstablished() { + return isEstablished; + } + + /** + * Performs authentication using provided principal name and password + * + * @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 = new HashSet<>(); + principals.add(new KerberosPrincipal(principal)); + + final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + + 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; + } + + /** + * Instead of jaas.conf, this requires refresh of {@link Configuration}. + */ + 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, options) }; + } + } + + 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; + } + } + } + } + } +} diff --git a/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf b/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf new file mode 100644 index 0000000000000..40abc19536637 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[libdefaults] + default_realm = {0} + udp_preference_limit = 1 + +[realms] + {0} = '{' + kdc = {1}:{2} + '}' diff --git a/x-pack/plugin/security/src/test/resources/minikdc.ldiff b/x-pack/plugin/security/src/test/resources/minikdc.ldiff new file mode 100644 index 0000000000000..5159abfa362fa --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/minikdc.ldiff @@ -0,0 +1,40 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +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 From 621104007c491b1dd4b54158ac7ea7c29d429926 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Mon, 4 Jun 2018 12:19:45 +1000 Subject: [PATCH 02/24] [Kerberos] Remove MiniKdc dependency The change removes MiniKdc dependency and disabling of license check for the same. --- x-pack/plugin/security/build.gradle | 2 - .../kerberos/KerberosAuthenticationToken.java | 6 +- .../support/KerberosTicketValidator.java | 5 +- .../kerberos/support/KerberosTestCase.java | 98 ++--- .../support/KerberosTicketValidatorTests.java | 30 +- .../authc/kerberos/support/MiniKdc.java | 352 ------------------ .../kerberos/support/SimpleKdcLdapServer.java | 220 +++++++++++ ...sts.java => SimpleKdcLdapServerTests.java} | 51 ++- .../security/src/test/resources/kdc.ldiff | 23 ++ .../src/test/resources/minikdc-krb5.conf | 25 -- .../security/src/test/resources/minikdc.ldiff | 40 -- 11 files changed, 316 insertions(+), 536 deletions(-) delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/{MiniKdcTests.java => SimpleKdcLdapServerTests.java} (66%) create mode 100644 x-pack/plugin/security/src/test/resources/kdc.ldiff delete mode 100644 x-pack/plugin/security/src/test/resources/minikdc-krb5.conf delete mode 100644 x-pack/plugin/security/src/test/resources/minikdc.ldiff diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 071e79f42c206..4032dd270032b 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -148,8 +148,6 @@ dependencyLicenses { licenseHeaders { // This class was sourced from apache directory studio for some microsoft-specific logic excludes << 'org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySIDUtil.java' - // This class was sourced from Hadoop as a wrapper around Apache DS, used only for tests - excludes << 'org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java' } forbiddenPatterns { 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 f1e22f7569d35..f2f3ae808efb1 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 @@ -95,7 +95,7 @@ public int hashCode() { } @Override - public boolean equals(Object other) { + public boolean equals(final Object other) { if (this == other) return true; if (other == null) @@ -106,8 +106,8 @@ public boolean equals(Object other) { return Objects.equals(otherKerbToken.principal(), principal()) && Objects.equals(otherKerbToken.credentials(), credentials()); } - private static ElasticsearchSecurityException unauthorized(String message, Throwable cause, Object... args) { - ElasticsearchSecurityException ese = new ElasticsearchSecurityException(message, RestStatus.UNAUTHORIZED, args); + 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_AUTH_HEADER); 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 index 726b47938da93..abbc2125c490e 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 @@ -101,7 +101,8 @@ public Tuple validateTicket(final String servicePrincipalName, O if (outToken != null && outToken.length > 0) { base64OutToken = Base64.getEncoder().encodeToString(outToken); } - + LOGGER.debug("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.getException() instanceof LoginException) { @@ -147,7 +148,7 @@ private static GSSCredential createCredentials(final String servicePrincipalName * @return * @throws PrivilegedActionException */ - private static T doAsWrapper(Subject subject, PrivilegedExceptionAction action) 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) { 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 c5a1cdea54fc6..ec044c49e9c78 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 @@ -6,11 +6,9 @@ package org.elasticsearch.xpack.security.authc.kerberos.support; -import com.unboundid.ldap.listener.InMemoryDirectoryServer; -import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; - +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; @@ -25,6 +23,8 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; import javax.security.auth.Subject; @@ -33,76 +33,58 @@ */ public abstract class KerberosTestCase extends ESTestCase { - protected MiniKdc miniKdc; - protected Path miniKdcWorkDir = null; - protected InMemoryDirectoryServer ldapServer; + protected Settings globalSettings; + protected Settings settings; + protected List serviceUserNames = new ArrayList<>(); + protected List clientUserNames = new ArrayList<>(); + protected Path workDir = null; + + protected SimpleKdcLdapServer simpleKdcLdapServer; @Before public void startMiniKdc() throws Exception { - createLdapService(); - createMiniKdc(); - AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public Void run() throws Exception { - miniKdc.start(); - return null; - } - }); - } + workDir = createTempDir(); + globalSettings = Settings.builder().put("path.home", workDir).build(); - @SuppressForbidden(reason = "Test dependency MiniKdc requires java.io.File and needs access to private field") - private void createMiniKdc() throws Exception { - miniKdcWorkDir = createTempDir(); - String backendConf = "kdc_identity_backend = org.apache.kerby.kerberos.kdc.identitybackend.LdapIdentityBackend\n" - + "host=127.0.0.1\n" + "port=" + ldapServer.getListenPort() + "\n" + "admin_dn=uid=admin,ou=system,dc=example,dc=com\n" - + "admin_pw=secret\n" + "base_dn=dc=example,dc=com"; - Files.write(miniKdcWorkDir.resolve("backend.conf"), backendConf.getBytes(StandardCharsets.UTF_8)); - assertTrue(Files.exists(miniKdcWorkDir.resolve("backend.conf"))); - miniKdc = new MiniKdc(MiniKdc.createConf(), miniKdcWorkDir.toFile()); - } + final Path kdcLdiff = getDataPath("/kdc.ldiff"); + simpleKdcLdapServer = new SimpleKdcLdapServer(workDir, "com", "example", kdcLdiff); - private void createLdapService() throws Exception { - InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com"); - config.setSchema(null); - ldapServer = new InMemoryDirectoryServer(config); - ldapServer.importFromLDIF(true, getDataPath("/minikdc.ldiff").toString()); - // Must have privileged access because underlying server will accept socket - // connections - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - ldapServer.startListening(); - return null; + // Create SPNs and UPNs + serviceUserNames.clear(); + Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { + serviceUserNames.add("HTTP/" + randomAlphaOfLength(6)); }); + final Path ktabPathForService = createPrincipalKeyTab(workDir, serviceUserNames.toArray(new String[0])); + clientUserNames.clear(); + Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { + String clientUserName = "client-" + randomAlphaOfLength(6); + 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 { - AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public Void run() throws Exception { - miniKdc.stop(); - return null; - } - }); - - ldapServer.shutDown(true); + simpleKdcLdapServer.stop(); } - @SuppressForbidden(reason = "Test dependency MiniKdc requires java.io.File") - protected Path createPrincipalKeyTab(final Path dir, final String... principalNames) throws Exception { - final Path ktabPath = dir.resolve(randomAlphaOfLength(10) + ".keytab"); - miniKdc.createPrincipal(ktabPath.toFile(), principalNames); - assertTrue(Files.exists(ktabPath)); - return ktabPath; + 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; } protected void createPrincipal(final String principalName, final char[] password) throws Exception { - miniKdc.createPrincipal(principalName, new String(password)); + simpleKdcLdapServer.createPrincipal(principalName, new String(password)); } protected String principalName(final String user) { - return user + "@" + miniKdc.getRealm(); + return user + "@" + simpleKdcLdapServer.getRealm(); } /** @@ -114,7 +96,7 @@ protected String principalName(final String user) { * @return * @throws PrivilegedActionException */ - public static T doAsWrapper(Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + public static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction action) throws PrivilegedActionException { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); } @@ -132,7 +114,7 @@ public static Settings buildKerberosRealmSettings(final String keytabPath) { public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, final boolean enableDebugging) { - Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) + 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); 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 75d142a79c107..2a1094db1527d 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 @@ -8,7 +8,6 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.RealmConfig; @@ -19,48 +18,27 @@ import org.hamcrest.Matchers; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSName; -import org.junit.Before; import org.junit.Rule; import org.junit.rules.ExpectedException; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.security.PrivilegedActionException; -import java.util.ArrayList; import java.util.Base64; -import java.util.List; import javax.security.auth.login.LoginException; public class KerberosTicketValidatorTests extends KerberosTestCase { - private Settings settings; - private List serviceUserNames = new ArrayList<>(); - private String clientUserName; - private Settings globalSettings; - private Path dir; private KerberosTicketValidator kerberosTicketValidator = new KerberosTicketValidator(); @Rule public ExpectedException thrown = ExpectedException.none(); - @Before - public void setup() throws Exception { - dir = createTempDir(); - globalSettings = Settings.builder().put("path.home", dir).build(); - serviceUserNames.clear(); - serviceUserNames.add("HTTP/" + randomAlphaOfLength(10)); - serviceUserNames.add("HTTP/" + randomAlphaOfLength(10)); - Path ktabPathForService = createPrincipalKeyTab(dir, serviceUserNames.toArray(new String[0])); - settings = buildKerberosRealmSettings(ktabPathForService.toString()); - clientUserName = "client-" + randomAlphaOfLength(5); - createPrincipal(clientUserName, "pwd".toCharArray()); - } - public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Exception { - createPrincipalKeyTab(dir, "differentServer"); + createPrincipalKeyTab(workDir, "differentServer"); // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer")); final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); @@ -95,12 +73,13 @@ public void testWhenKeyTabDoesNotExistFailsValidation() throws LoginException, G public void testWhenKeyTabWithInvalidContentFailsValidation() throws LoginException, GSSException, IOException, PrivilegedActionException { // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames))); final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); assertNotNull(base64KerbToken); - final Path ktabPath = writeKeyTab(dir, "invalid.keytab", "not - a - valid - key - tab"); + final Path ktabPath = writeKeyTab(workDir, "invalid.keytab", "not - a - valid - key - tab"); settings = buildKerberosRealmSettings(ktabPath.toString()); final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); @@ -110,6 +89,7 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() public void testValidKebrerosTicket() throws PrivilegedActionException, GSSException, LoginException { // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames))); final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java deleted file mode 100644 index 6d864fa96fc4c..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdc.java +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.elasticsearch.xpack.security.authc.kerberos.support; - -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.IOUtil; -import org.apache.kerby.util.NetworkUtil; -import org.elasticsearch.common.SuppressForbidden; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - -/** - * Mini KDC based on Apache Directory Server that can be embedded in testcases - * or used from command line as a standalone KDC. - *

- * From within testcases: - *

- * MiniKdc sets one System property when started and un-set when stopped: - *

    - *
  • sun.security.krb5.debug: set to the debug value provided in the - * configuration
  • - *
- * Because of this, multiple MiniKdc instances cannot be started in parallel. - * For example, running testcases in parallel that start a KDC each. To - * accomplish this a single MiniKdc should be used for all testcases running in - * parallel. - *

- * MiniKdc default configuration values are: - *

    - *
  • org.name=EXAMPLE (used to create the REALM)
  • - *
  • org.domain=COM (used to create the REALM)
  • - *
  • kdc.bind.address=localhost
  • - *
  • kdc.port=0 (ephemeral port)
  • - *
  • instance=DefaultKrbServer
  • - *
  • max.ticket.lifetime=86400000 (1 day)
  • - *
  • max.renewable.lifetime=604800000 (7 days)
  • - *
  • transport=TCP
  • - *
  • debug=false
  • - *
- * The generated krb5.conf forces TCP connections. - */ -@SuppressForbidden(reason = "Uses apache simple kdc server requires does not yet support java.nio.file") -public class MiniKdc { - - public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf"; - public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug"; - private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class); - - public static final String ORG_NAME = "org.name"; - public static final String ORG_DOMAIN = "org.domain"; - public static final String KDC_BIND_ADDRESS = "kdc.bind.address"; - public static final String KDC_PORT = "kdc.port"; - public static final String INSTANCE = "instance"; - public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime"; - public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime"; - public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime"; - public static final String TRANSPORT = "transport"; - public static final String DEBUG = "debug"; - - private static final Set PROPERTIES = new HashSet(); - private static final Properties DEFAULT_CONFIG = new Properties(); - - static { - PROPERTIES.add(ORG_NAME); - PROPERTIES.add(ORG_DOMAIN); - PROPERTIES.add(KDC_BIND_ADDRESS); - PROPERTIES.add(KDC_BIND_ADDRESS); - PROPERTIES.add(KDC_PORT); - PROPERTIES.add(INSTANCE); - PROPERTIES.add(TRANSPORT); - PROPERTIES.add(MAX_TICKET_LIFETIME); - PROPERTIES.add(MAX_RENEWABLE_LIFETIME); - - DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost"); - DEFAULT_CONFIG.setProperty(KDC_PORT, "0"); - DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer"); - DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE"); - DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM"); - DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP"); - DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000"); - DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000"); - DEFAULT_CONFIG.setProperty(DEBUG, "false"); - } - - /** - * Convenience method that returns MiniKdc default configuration. - *

- * The returned configuration is a copy, it can be customized before using it to - * create a MiniKdc. - * - * @return a MiniKdc default configuration. - */ - public static Properties createConf() { - return (Properties) DEFAULT_CONFIG.clone(); - } - - private Properties conf; - private SimpleKdcServer simpleKdc; - private int port; - private String realm; - private File workDir; - private File krb5conf; - private String transport; - private boolean krb5Debug; - - public void setTransport(String transport) { - this.transport = transport; - } - - /** - * Creates a MiniKdc. - * - * @param conf MiniKdc configuration. - * @param workDir working directory, it should be the build directory. Under - * this directory an ApacheDS working directory will be created, this - * directory will be deleted when the MiniKdc stops. - * @throws Exception thrown if the MiniKdc could not be created. - */ - public MiniKdc(Properties conf, File workDir) throws Exception { - if (!conf.keySet().containsAll(PROPERTIES)) { - Set missingProperties = new HashSet(PROPERTIES); - missingProperties.removeAll(conf.keySet()); - throw new IllegalArgumentException("Missing configuration properties: " + missingProperties); - } - this.workDir = new File(workDir, Long.toString(System.currentTimeMillis())); - if (!this.workDir.exists() && !this.workDir.mkdirs()) { - throw new RuntimeException("Cannot create directory " + this.workDir); - } - LOG.info("Configuration:"); - LOG.info("---------------------------------------------------------------"); - for (Map.Entry entry : conf.entrySet()) { - LOG.info(" {}: {}", entry.getKey(), entry.getValue()); - } - LOG.info("---------------------------------------------------------------"); - this.conf = conf; - port = Integer.parseInt(conf.getProperty(KDC_PORT)); - String orgName = conf.getProperty(ORG_NAME); - String orgDomain = conf.getProperty(ORG_DOMAIN); - realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH); - } - - /** - * Returns the port of the MiniKdc. - * - * @return the port of the MiniKdc. - */ - public int getPort() { - return port; - } - - /** - * Returns the host of the MiniKdc. - * - * @return the host of the MiniKdc. - */ - public String getHost() { - return conf.getProperty(KDC_BIND_ADDRESS); - } - - /** - * Returns the realm of the MiniKdc. - * - * @return the realm of the MiniKdc. - */ - public String getRealm() { - return realm; - } - - public File getKrb5conf() { - krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF)); - return krb5conf; - } - - /** - * Starts the MiniKdc. - * - * @throws Exception thrown if the MiniKdc could not be started. - */ - public synchronized void start() throws Exception { - if (simpleKdc != null) { - throw new RuntimeException("Already started"); - } - simpleKdc = new SimpleKdcServer(this.workDir.getParentFile(), new KrbConfig()); - simpleKdc.setKdcRealm("EXAMPLE.COM"); - simpleKdc.setKdcHost("localhost"); - simpleKdc.setKdcPort(NetworkUtil.getServerPort()); - - prepareKdcServer(); - simpleKdc.init(); - resetDefaultRealm(); - simpleKdc.start(); - LOG.info("MiniKdc started."); - } - - private void resetDefaultRealm() throws IOException { - InputStream templateResource = new FileInputStream(getKrb5conf().getAbsolutePath()); - String content = IOUtil.readInput(templateResource); - content = content.replaceAll("default_realm = .*\n", "default_realm = " + getRealm() + "\n"); - IOUtil.writeFile(content, getKrb5conf()); - } - - private void prepareKdcServer() throws Exception { - // transport - simpleKdc.setWorkDir(workDir); - simpleKdc.setKdcHost(getHost()); - simpleKdc.setKdcRealm(realm); - if (transport == null) { - transport = conf.getProperty(TRANSPORT); - } - if (port == 0) { - port = NetworkUtil.getServerPort(); - } - if (transport != null) { - if (transport.trim().equals("TCP")) { - simpleKdc.setKdcTcpPort(port); - simpleKdc.setAllowUdp(false); - } else if (transport.trim().equals("UDP")) { - simpleKdc.setKdcUdpPort(port); - simpleKdc.setAllowTcp(false); - } else { - throw new IllegalArgumentException("Invalid transport: " + transport); - } - } else { - throw new IllegalArgumentException("Need to set transport!"); - } - simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, conf.getProperty(INSTANCE)); - if (conf.getProperty(DEBUG) != null) { - krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG)); - } - if (conf.getProperty(MIN_TICKET_LIFETIME) != null) { - simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, Long.parseLong(conf.getProperty(MIN_TICKET_LIFETIME))); - } - if (conf.getProperty(MAX_TICKET_LIFETIME) != null) { - simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, - Long.parseLong(conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME))); - } - } - - /** - * Stops the MiniKdc - */ - public synchronized void stop() { - if (simpleKdc != null) { - try { - simpleKdc.stop(); - } catch (KrbException e) { - e.printStackTrace(); - } finally { - if (conf.getProperty(DEBUG) != null) { - System.setProperty(SUN_SECURITY_KRB5_DEBUG, Boolean.toString(krb5Debug)); - } - } - } - delete(workDir); - try { - // Will be fixed in next Kerby version. - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - LOG.info("MiniKdc stopped."); - } - - private void delete(File f) { - if (f.isFile()) { - if (!f.delete()) { - LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath()); - } - } else { - File[] fileList = f.listFiles(); - if (fileList != null) { - for (File c : fileList) { - delete(c); - } - } - if (!f.delete()) { - LOG.warn("WARNING: cannot delete directory " + f.getAbsolutePath()); - } - } - } - - /** - * 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(String principal, 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. - * @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. - */ - public synchronized void createPrincipal(File keytabFile, String... principals) throws Exception { - simpleKdc.createPrincipals(principals); - if (keytabFile.exists() && !keytabFile.delete()) { - LOG.error("Failed to delete keytab file: " + keytabFile); - } - for (String principal : principals) { - simpleKdc.getKadmin().exportKeytab(keytabFile, principal); - } - } - - /** - * Set the System property; return the old value for caching. - * - * @param sysprop property - * @param debug true or false - * @return the previous value - */ - private boolean getAndSet(String sysprop, String debug) { - boolean old = Boolean.getBoolean(sysprop); - System.setProperty(sysprop, debug); - return old; - } -} 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..32a496eae7203 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServer.java @@ -0,0 +1,220 @@ +/* + * 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.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.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; + +/** + * 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. + */ +@SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") +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 them with provided configuration. + * + * @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 = orgName.toUpperCase(Locale.ROOT) + "." + domainName.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 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."); + } + + 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 = + "kdc_identity_backend = 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")); + } + + 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!"); + } + 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. + */ + 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 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)); + } + } + + try { + // Will be fixed in next Kerby version. + Thread.sleep(1000); + } catch (InterruptedException e) { + throw ExceptionsHelper.convertToRuntime(e); + } + + if (ldapServer != null) { + ldapServer.shutDown(true); + } + return null; + } + }); + logger.info("SimpleKdcServer stoppped."); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java similarity index 66% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java index 519535fbc2ff8..6aaa9bb541e96 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/MiniKdcTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java @@ -6,54 +6,47 @@ 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.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTicketValidator; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSName; -import org.junit.After; -import org.junit.Before; -import java.io.IOException; -import java.nio.file.Path; +import java.nio.file.Files; +import java.security.AccessController; import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import javax.security.auth.login.LoginException; -/** - * Tests MiniKdc and framework around it. - */ -public class MiniKdcTests extends KerberosTestCase { - - private Settings settings; - private String serviceUserName; - private String clientUserName; - private Settings globalSettings; - private Path dir; +public class SimpleKdcLdapServerTests extends KerberosTestCase { - @Before - public void setup() throws Exception { - dir = createTempDir(); - globalSettings = Settings.builder().put("path.home", dir).build(); - serviceUserName = "HTTP/" + randomAlphaOfLength(10); - Path ktabPathForService = createPrincipalKeyTab(dir, serviceUserName); - settings = buildKerberosRealmSettings(ktabPathForService.toString()); - clientUserName = "client-" + randomAlphaOfLength(10); - createPrincipal(clientUserName, "pwd".toCharArray()); - } + public void testPrincipalCreationAndSearchOnLdap() throws Exception { + simpleKdcLdapServer.createPrincipal(workDir.resolve("p1p2.keytab"), "p1", "p2"); + assertTrue(Files.exists(workDir.resolve("p1p2.keytab"))); + LDAPConnection ldapConn = AccessController.doPrivileged(new PrivilegedExceptionAction() { - @After - public void cleanup() throws IOException { + @Override + public LDAPConnection run() throws Exception { + return new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()); + } + }); + assertTrue(ldapConn.isConnected()); + SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(uid=p1)"); + assertEquals(1, sr.getEntryCount()); } public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException { + final String serviceUserName = randomFrom(serviceUserNames); // Client login and init token preparation + final String clientUserName = randomFrom(clientUserNames); final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName)); final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); 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 diff --git a/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf b/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf deleted file mode 100644 index 40abc19536637..0000000000000 --- a/x-pack/plugin/security/src/test/resources/minikdc-krb5.conf +++ /dev/null @@ -1,25 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -[libdefaults] - default_realm = {0} - udp_preference_limit = 1 - -[realms] - {0} = '{' - kdc = {1}:{2} - '}' diff --git a/x-pack/plugin/security/src/test/resources/minikdc.ldiff b/x-pack/plugin/security/src/test/resources/minikdc.ldiff deleted file mode 100644 index 5159abfa362fa..0000000000000 --- a/x-pack/plugin/security/src/test/resources/minikdc.ldiff +++ /dev/null @@ -1,40 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -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 From 29ef31c61def988c1c536c070528d2a46b9aac18 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 5 Jun 2018 08:52:01 +1000 Subject: [PATCH 03/24] [Kerberos] Address review comments Added few tests, modified KerberosAuthenticationToken to keep decoded bytes. Corresponding changes in KerberosTicketValidator and tests. --- .../kerberos/KerberosAuthenticationToken.java | 35 +++++++++---------- .../support/KerberosTicketValidator.java | 19 ++++------ .../KerberosAuthenticationTokenTests.java | 27 +++++++++++--- .../support/KerberosTicketValidatorTests.java | 12 +++---- .../support/SimpleKdcLdapServerTests.java | 5 +-- 5 files changed, 55 insertions(+), 43 deletions(-) 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 f2f3ae808efb1..4969ee10b3493 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 @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; @@ -27,12 +28,10 @@ public final class KerberosAuthenticationToken implements AuthenticationToken { public static final String NEGOTIATE_AUTH_HEADER = "Negotiate "; public static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; - private final String principalName; - private String base64EncodedTicket; + private byte[] decodedTicket; - public KerberosAuthenticationToken(final String base64EncodedToken) { - this.principalName = UNAUTHENTICATED_PRINCIPAL_NAME; - this.base64EncodedTicket = base64EncodedToken; + public KerberosAuthenticationToken(final byte[] base64EncodedToken) { + this.decodedTicket = base64EncodedToken; } /** @@ -50,7 +49,8 @@ public static KerberosAuthenticationToken extractToken(final ThreadContext conte return null; } - if (authHeader.startsWith(NEGOTIATE_AUTH_HEADER) == Boolean.FALSE) { + boolean ignoreCase = true; + if (authHeader.regionMatches(ignoreCase, 0, NEGOTIATE_AUTH_HEADER, 0, NEGOTIATE_AUTH_HEADER.length()) == Boolean.FALSE) { return null; } @@ -60,38 +60,35 @@ public static KerberosAuthenticationToken extractToken(final ThreadContext conte } final byte[] base64Token = base64EncodedToken.getBytes(StandardCharsets.UTF_8); byte[] decodedKerberosTicket = null; - IllegalArgumentException rootCause = null; try { decodedKerberosTicket = Base64.getDecoder().decode(base64Token); } catch (IllegalArgumentException iae) { - rootCause = iae; - } - - if (decodedKerberosTicket == null || decodedKerberosTicket.length == 0) { - throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", rootCause, + throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", iae, base64EncodedToken); } - return new KerberosAuthenticationToken(base64EncodedToken); + + return new KerberosAuthenticationToken(decodedKerberosTicket); } @Override public String principal() { - return principalName; + return UNAUTHENTICATED_PRINCIPAL_NAME; } @Override public Object credentials() { - return base64EncodedTicket; + return decodedTicket; } @Override public void clearCredentials() { - this.base64EncodedTicket = null; + Arrays.fill(decodedTicket, (byte) 0); + this.decodedTicket = null; } @Override public int hashCode() { - return Objects.hash(principalName, base64EncodedTicket); + return Objects.hash(decodedTicket); } @Override @@ -103,12 +100,12 @@ public boolean equals(final Object other) { if (getClass() != other.getClass()) return false; final KerberosAuthenticationToken otherKerbToken = (KerberosAuthenticationToken) other; - return Objects.equals(otherKerbToken.principal(), principal()) && Objects.equals(otherKerbToken.credentials(), credentials()); + return Objects.equals(otherKerbToken.credentials(), credentials()); } 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_AUTH_HEADER); + ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER.trim()); 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 index abbc2125c490e..12ad4311a3e7b 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 @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.security.authc.kerberos.support; -import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.collect.Tuple; @@ -78,7 +77,7 @@ private static Oid getSpnegoOid() { * @throws GSSException thrown when GSS Context negotiation fails * {@link GSSException} */ - public Tuple validateTicket(final String servicePrincipalName, Oid nameType, final String base64Ticket, + public Tuple validateTicket(final String servicePrincipalName, Oid nameType, final byte[] decodedToken, final RealmConfig config) throws LoginException, GSSException { final GSSManager gssManager = GSSManager.getInstance(); GSSContext gssContext = null; @@ -89,19 +88,16 @@ public Tuple validateTicket(final String servicePrincipalName, O throw new IllegalArgumentException("configured service key tab file does not exist for " + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH)); } - // do service login loginContext = serviceLogin(servicePrincipalName, keyTabPath.toString(), config.settings()); - // create credentials GSSCredential serviceCreds = createCredentials(servicePrincipalName, nameType, gssManager, loginContext); - // create gss context gssContext = gssManager.createContext(serviceCreds); - final byte[] outToken = acceptSecContext(base64Ticket, gssContext, loginContext); + final byte[] outToken = acceptSecContext(decodedToken, gssContext, loginContext); String base64OutToken = null; if (outToken != null && outToken.length > 0) { base64OutToken = Base64.getEncoder().encodeToString(outToken); } - LOGGER.debug("validateTicket isGSSContextEstablished = {}, username = {}, outToken = {}", gssContext.isEstablished(), + 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) { @@ -118,13 +114,12 @@ public Tuple validateTicket(final String servicePrincipalName, O } } - private static byte[] acceptSecContext(final String base64Ticket, GSSContext gssContext, LoginContext loginContext) + private static byte[] acceptSecContext(final byte[] base64Ticket, GSSContext gssContext, LoginContext loginContext) throws PrivilegedActionException { final GSSContext finalGSSContext = gssContext; - final byte[] token = Base64.getDecoder().decode(base64Ticket); // process token with gss context return doAsWrapper(loginContext.getSubject(), - (PrivilegedExceptionAction) () -> finalGSSContext.acceptSecContext(token, 0, token.length)); + (PrivilegedExceptionAction) () -> finalGSSContext.acceptSecContext(base64Ticket, 0, base64Ticket.length)); } private static GSSCredential createCredentials(final String servicePrincipalName, Oid nameType, final GSSManager gssManager, @@ -171,7 +166,7 @@ private static void privilegedCloseNoThrow(final GSSContext gssContext) { }); } catch (PrivilegedActionException e) { RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); - LOGGER.log(Level.DEBUG, "Could not dispose GSS Context", rte); + LOGGER.debug("Could not dispose GSS Context", rte); } return; } @@ -192,7 +187,7 @@ private static void privilegedDisposeNoThrow(final LoginContext loginContext) { }); } catch (PrivilegedActionException e) { RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); - LOGGER.log(Level.DEBUG, "Could not close LoginContext", rte); + LOGGER.debug("Could not close LoginContext", rte); } } } 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 index e4f9bb743f86b..08737a5cc8e2f 100644 --- 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 @@ -49,12 +49,14 @@ public void cleanup() throws IOException { public void testExtractTokenForValidAuthorizationHeader() throws IOException { final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); + final String negotiate = randomBoolean() ? KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER : "negotiate "; + threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, negotiate + base64Token); final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); assertNotNull(kerbAuthnToken); assertEquals(UNAUTHENTICATED_PRINCIPAL_NAME, kerbAuthnToken.principal()); - assertEquals(base64Token, kerbAuthnToken.credentials()); + assertTrue(kerbAuthnToken.credentials() instanceof byte[]); + assertArrayEquals(Base64.getDecoder().decode(base64Token), (byte[]) kerbAuthnToken.credentials()); } public void testExtractTokenForInvalidAuthorizationHeaderThrowsException() throws IOException { @@ -135,9 +137,26 @@ public void testKerberoAuthenticationTokenClearCredentials() { } public void testEqualsHashCode() { - final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken("base64EncodedToken"); + final KerberosAuthenticationToken kerberosAuthenticationToken = + new KerberosAuthenticationToken("base64EncodedToken".getBytes(StandardCharsets.UTF_8)); EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { - return new KerberosAuthenticationToken((String) original.credentials()); + return new KerberosAuthenticationToken((byte[]) original.credentials()); }); + 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"); + } } } 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 2a1094db1527d..c5aab56eb1f03 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 @@ -47,7 +47,7 @@ public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Ex final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); } public void testInvalidKerbTicketFailsValidation() throws Exception { @@ -56,7 +56,7 @@ public void testInvalidKerbTicketFailsValidation() throws Exception { final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); thrown.expect(new GSSExceptionMatcher(GSSException.DEFECTIVE_TOKEN)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); } public void testWhenKeyTabDoesNotExistFailsValidation() throws LoginException, GSSException { @@ -67,7 +67,7 @@ public void testWhenKeyTabDoesNotExistFailsValidation() throws LoginException, G thrown.expect(IllegalArgumentException.class); thrown.expectMessage(Matchers.equalTo("configured service key tab file does not exist for " + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH))); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); } public void testWhenKeyTabWithInvalidContentFailsValidation() @@ -84,7 +84,7 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, base64KerbToken, config); + kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); } public void testValidKebrerosTicket() throws PrivilegedActionException, GSSException, LoginException { @@ -97,8 +97,8 @@ public void testValidKebrerosTicket() throws PrivilegedActionException, GSSExcep final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - final Tuple userNameOutToken = - kerberosTicketValidator.validateTicket("*", GSSName.NT_HOSTBASED_SERVICE, base64KerbToken, config); + final Tuple userNameOutToken = kerberosTicketValidator.validateTicket("*", GSSName.NT_HOSTBASED_SERVICE, + Base64.getDecoder().decode(base64KerbToken), config); assertNotNull(userNameOutToken); assertEquals(principalName(clientUserName), userNameOutToken.v1()); assertNotNull(userNameOutToken.v2()); 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 6aaa9bb541e96..557b851c7779c 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 @@ -23,6 +23,7 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Base64; import javax.security.auth.login.LoginException; @@ -51,14 +52,14 @@ public void testClientServiceMutualAuthentication() throws PrivilegedActionExcep new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName)); final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); assertNotNull(base64KerbToken); - final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(base64KerbToken); + final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); // Service Login final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); // Handle Authz header which contains base64 token final Tuple userNameOutToken = new KerberosTicketValidator().validateTicket(principalName(serviceUserName), - GSSName.NT_USER_NAME, (String) kerbAuthnToken.credentials(), config); + GSSName.NT_USER_NAME, (byte[]) kerbAuthnToken.credentials(), config); assertNotNull(userNameOutToken); assertEquals(principalName(clientUserName), userNameOutToken.v1()); From 4fe4f54fe3865501c040536ed7a8b7dd6b6fc6d6 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 5 Jun 2018 10:00:16 +1000 Subject: [PATCH 04/24] [Kerberos] Fix the test, removed duplicate code --- .../KerberosAuthenticationTokenTests.java | 57 +++++++------------ 1 file changed, 21 insertions(+), 36 deletions(-) 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 index 08737a5cc8e2f..db26bb8aac8b1 100644 --- 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 @@ -27,6 +27,7 @@ import java.util.List; public class KerberosAuthenticationTokenTests extends ESTestCase { + private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; private ThreadContext threadContext; @@ -65,24 +66,7 @@ public void testExtractTokenForInvalidAuthorizationHeaderThrowsException() throw thrown.expect(ElasticsearchSecurityException.class); thrown.expectMessage( Matchers.equalTo("invalid negotiate authentication header value, expected base64 encoded token but value is empty")); - thrown.expect(new BaseMatcher() { - - @Override - public boolean matches(Object item) { - if (item instanceof ElasticsearchSecurityException) { - List authHeaderValue = - ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); - if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.equals(authHeaderValue.get(0))) { - return true; - } - } - return false; - } - - @Override - public void describeTo(Description description) { - } - }); + thrown.expect(new ESEMatcher()); KerberosAuthenticationToken.extractToken(threadContext); fail("Expected exception not thrown"); } @@ -95,24 +79,7 @@ public void testExtractTokenForNotBase64EncodedTokenThrowsException() throws IOE thrown.expect(ElasticsearchSecurityException.class); thrown.expectMessage( Matchers.equalTo("invalid negotiate authentication header value, could not decode base64 token " + notBase64Token)); - thrown.expect(new BaseMatcher() { - - @Override - public boolean matches(Object item) { - if (item instanceof ElasticsearchSecurityException) { - List authHeaderValue = - ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); - if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.equals(authHeaderValue.get(0))) { - return true; - } - } - return false; - } - - @Override - public void describeTo(Description description) { - } - }); + thrown.expect(new ESEMatcher()); KerberosAuthenticationToken.extractToken(threadContext); fail("Expected exception not thrown"); } @@ -159,4 +126,22 @@ private static KerberosAuthenticationToken mutateTestItem(KerberosAuthentication throw new IllegalArgumentException("unknown option"); } } + + private static class ESEMatcher extends BaseMatcher { + @Override + public boolean matches(Object item) { + if (item instanceof ElasticsearchSecurityException) { + List authHeaderValue = + ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); + if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.contains(authHeaderValue.get(0))) { + return true; + } + } + return false; + } + + @Override + public void describeTo(Description description) { + } + } } From 542c41d2ea2b18cf278c506bbbaf789cb77d8a24 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 5 Jun 2018 11:43:28 +1000 Subject: [PATCH 05/24] [Kerberos] Address random test failure This was due to less minimum ticket lifetime set on kdc. --- .../kerberos/support/KerberosTestCase.java | 5 +- .../kerberos/support/SimpleKdcLdapServer.java | 10 +++- .../authc/kerberos/support/SpnegoClient.java | 60 +++++++++++++++---- 3 files changed, 61 insertions(+), 14 deletions(-) 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 ec044c49e9c78..4f0faf571124c 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 @@ -43,6 +43,7 @@ public abstract class KerberosTestCase extends ESTestCase { @Before public void startMiniKdc() throws Exception { + workDir = createTempDir(); globalSettings = Settings.builder().put("path.home", workDir).build(); @@ -52,12 +53,12 @@ public void startMiniKdc() throws Exception { // Create SPNs and UPNs serviceUserNames.clear(); Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { - serviceUserNames.add("HTTP/" + randomAlphaOfLength(6)); + serviceUserNames.add("HTTP/" + randomAlphaOfLength(8)); }); final Path ktabPathForService = createPrincipalKeyTab(workDir, serviceUserNames.toArray(new String[0])); clientUserNames.clear(); Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { - String clientUserName = "client-" + randomAlphaOfLength(6); + String clientUserName = "client-" + randomAlphaOfLength(8); clientUserNames.add(clientUserName); try { createPrincipal(clientUserName, "pwd".toCharArray()); 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 index 32a496eae7203..3ceb10e929967 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/support/SimpleKdcLdapServer.java @@ -11,6 +11,7 @@ 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; @@ -66,7 +67,7 @@ public class SimpleKdcLdapServer { */ public SimpleKdcLdapServer(final Path workDir, final String orgName, final String domainName, final Path ldiff) throws Exception { this.workDir = workDir; - this.realm = orgName.toUpperCase(Locale.ROOT) + "." + domainName.toUpperCase(Locale.ROOT); + 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() { @@ -137,8 +138,15 @@ private void prepareKdcServerAndStart() throws Exception { } else { throw new IllegalArgumentException("Need to set transport!"); } + long minimumTicketLifeTime = simpleKdc.getKdcConfig().getMinimumTicketLifetime(); + long maxRenewableLifeTime = simpleKdc.getKdcConfig().getMaximumRenewableLifetime(); + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, 86400000L); + simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 604800000L); + logger.info("MINIMUM_TICKET_LIFETIME changed from {} to {}", minimumTicketLifeTime, 86400000L); + logger.info("MAXIMUM_RENEWABLE_LIFETIME changed from {} to {}", maxRenewableLifeTime, 604800000L); simpleKdc.init(); simpleKdc.start(); + } public String getRealm() { 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 index 3c88d1041b85c..b3ae02417c933 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/support/SpnegoClient.java @@ -6,7 +6,10 @@ 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; @@ -43,6 +46,7 @@ * Not thread safe */ public class SpnegoClient { + private static final Logger LOGGER = ESLoggerFactory.getLogger(SpnegoClient.class); public static final Oid SPNEGO_OID = getSpnegoOid(); private static Oid getSpnegoOid() { @@ -64,17 +68,28 @@ private static Oid getSpnegoOid() { public SpnegoClient(final String userPrincipalName, final SecureString password, final String servicePrincipalName) throws PrivilegedActionException, GSSException { - 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, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); - gssContext = gssManager.createContext(gssServicePrincipalName.canonicalize(SPNEGO_OID), SPNEGO_OID, userCreds, - GSSCredential.DEFAULT_LIFETIME); - gssContext.requestMutualAuth(true); - isEstablished = gssContext.isEstablished(); + String oldUseSubjectCredsOnlyFlag = null; + try { + oldUseSubjectCredsOnlyFlag = getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", "true"); + + LOGGER.info("SpnegoClient with princName : {}", 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, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); + gssContext = gssManager.createContext(gssServicePrincipalName.canonicalize(SPNEGO_OID), SPNEGO_OID, userCreds, + GSSCredential.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + isEstablished = gssContext.isEstablished(); + } catch (PrivilegedActionException pve) { + LOGGER.error("privileged action exception, with root cause", pve.getException()); + throw pve; + } finally { + getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", oldUseSubjectCredsOnlyFlag); + } } public String getBase64TicketForSpnegoHeader() throws PrivilegedActionException { @@ -187,4 +202,27 @@ public void handle(final Callback[] callbacks) throws IOException, UnsupportedCa } } } + + private static String getAndSetSystemProperty(final String systemProperty, final String value) { + String retVal = null; + try { + retVal = AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + @SuppressForbidden( + reason = "For testing we want to provide credentials, so setting system property javax.security.auth.useSubjectCredsOnly") + public String run() throws Exception { + String oldValue = System.getProperty(systemProperty); + if (value != null) { + System.setProperty(systemProperty, value); + } + return oldValue; + } + + }); + } catch (PrivilegedActionException e) { + throw ExceptionsHelper.convertToRuntime(e); + } + return retVal; + } } From 067a86306d3cc4d26cb7ccae2c8e440bc374a5b4 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 5 Jun 2018 15:36:32 +1000 Subject: [PATCH 06/24] [Kerberos] Line length check failed --- .../xpack/security/authc/kerberos/support/SpnegoClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b3ae02417c933..c43c6f896528b 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/support/SpnegoClient.java @@ -210,7 +210,7 @@ private static String getAndSetSystemProperty(final String systemProperty, final @Override @SuppressForbidden( - reason = "For testing we want to provide credentials, so setting system property javax.security.auth.useSubjectCredsOnly") + reason = "For testing application provides credentials, needs sys prop javax.security.auth.useSubjectCredsOnly") public String run() throws Exception { String oldValue = System.getProperty(systemProperty); if (value != null) { From 837532e6a54bcce27c55984de324d85ebbd5f2de Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Tue, 5 Jun 2018 20:04:20 +1000 Subject: [PATCH 07/24] [Kerberos] Did not check the results for javadoc, fixed it. --- .../authc/kerberos/support/KerberosTicketValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 12ad4311a3e7b..78ed32fc8d196 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 @@ -67,7 +67,7 @@ private static Oid getSpnegoOid() { * * @param servicePrincipalName Service principal name * @param nameType {@link GSSName} type depending on GSS API principal entity - * @param base64Ticket base64 encoded kerberos ticket + * @param decodedToken base64 decoded kerberos ticket bytes * @param config {@link RealmConfig} * @return {@link Tuple} of user name {@link GSSContext#getSrcName()} and out * token base64 encoded if any. When context is not yet established user From b4f2f007126dad5151b73b0379124062516a30f9 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 8 Jun 2018 13:34:52 +1000 Subject: [PATCH 08/24] [Kerberos] Address review comments. This commit addresses review comments. Also during testing found Arabic "ar" locale having problem with SimpleKdcServer so added a check for it. Removed usage of Junit rules, using expectThrows instead. Add missing java docs. --- .../authc/kerberos/KerberosRealmSettings.java | 14 +- .../kerberos/KerberosAuthenticationToken.java | 53 ++--- .../support/KerberosTicketValidator.java | 195 ++++++++++-------- .../KerberosAuthenticationTokenTests.java | 115 ++++------- .../kerberos/KerberosRealmSettingsTests.java | 9 +- .../kerberos/support/KerberosTestCase.java | 111 +++++++++- .../support/KerberosTicketValidatorTests.java | 132 +++++------- .../kerberos/support/SimpleKdcLdapServer.java | 13 +- .../support/SimpleKdcLdapServerTests.java | 47 +++-- .../authc/kerberos/support/SpnegoClient.java | 87 +++++--- 10 files changed, 418 insertions(+), 358 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 3fac552ccaa58..85fd2ecbf3e43 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 @@ -20,26 +20,26 @@ public final class KerberosRealmSettings { public static final String TYPE = "kerberos"; /** - * Kerberos Key tab for Elasticsearch HTTP Service and Kibana HTTP Service
+ * Kerberos key tab for Elasticsearch service
* Uses single key tab for multiple service accounts. */ public static final Setting HTTP_SERVICE_KEYTAB_PATH = - Setting.simpleString("http.service.keytab.path", Setting.Property.NodeScope); + Setting.simpleString("keytab.path", Property.NodeScope); public static final Setting SETTING_KRB_DEBUG_ENABLE = - Setting.boolSetting("krb.debug", Boolean.FALSE, Setting.Property.Dynamic, Property.NodeScope); + Setting.boolSetting("krb.debug", Boolean.FALSE, Property.NodeScope); + // Cache private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); - public static final Setting CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); 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, Setting.Property.NodeScope); + Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Property.NodeScope); private KerberosRealmSettings() { - /* Empty private constructor */ } /** - * @return Set of {@link Setting}s for {@value #TYPE} + * @return the valid set of {@link Setting}s for a {@value #TYPE} realm */ public static Set> getSettings() { Set> settings = new HashSet<>(); 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 4969ee10b3493..f697963cb6f03 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 @@ -8,7 +8,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -18,43 +17,53 @@ import java.util.Objects; /** - * Holds on to base 64 encoded ticket, also helps extracting token from - * {@link ThreadContext} + * This class represents AuthenticationToken for Kerberos authentication using + * SPNEGO mechanism. 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_AUTH_HEADER = "Negotiate "; - public static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; - private byte[] decodedTicket; + private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; - public KerberosAuthenticationToken(final byte[] base64EncodedToken) { - this.decodedTicket = base64EncodedToken; + private final byte[] base64DecodedToken; + + public KerberosAuthenticationToken(final byte[] base64DecodedToken) { + this.base64DecodedToken = base64DecodedToken; } /** - * Extract token from header and if valid {@link #NEGOTIATE_AUTH_HEADER} then - * returns {@link KerberosAuthenticationToken} + * Extract token from authorization header and if it is valid + * {@link #NEGOTIATE_AUTH_HEADER} then returns + * {@link KerberosAuthenticationToken} * - * @param context {@link ThreadContext} + * @param authorizationHeader Authorization header from request * @return returns {@code null} if {@link #AUTH_HEADER} is empty or not an * {@link #NEGOTIATE_AUTH_HEADER} else returns valid * {@link KerberosAuthenticationToken} + * @throws ElasticsearchSecurityException when negotiate header is invalid. */ - public static KerberosAuthenticationToken extractToken(final ThreadContext context) { - final String authHeader = context.getHeader(AUTH_HEADER); - if (Strings.isNullOrEmpty(authHeader)) { + public static KerberosAuthenticationToken extractToken(final String authorizationHeader) { + if (Strings.isNullOrEmpty(authorizationHeader)) { return null; } - boolean ignoreCase = true; - if (authHeader.regionMatches(ignoreCase, 0, NEGOTIATE_AUTH_HEADER, 0, NEGOTIATE_AUTH_HEADER.length()) == Boolean.FALSE) { + if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER.trim(), 0, + NEGOTIATE_AUTH_HEADER.trim().length()) == false) { return null; } - final String base64EncodedToken = authHeader.substring(NEGOTIATE_AUTH_HEADER.length()).trim(); + final String base64EncodedToken = authorizationHeader.substring(NEGOTIATE_AUTH_HEADER.trim().length()).trim(); if (Strings.isEmpty(base64EncodedToken)) { throw unauthorized("invalid negotiate authentication header value, expected base64 encoded token but value is empty", null); } @@ -63,8 +72,7 @@ public static KerberosAuthenticationToken extractToken(final ThreadContext conte try { decodedKerberosTicket = Base64.getDecoder().decode(base64Token); } catch (IllegalArgumentException iae) { - throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", iae, - base64EncodedToken); + throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", iae, base64EncodedToken); } return new KerberosAuthenticationToken(decodedKerberosTicket); @@ -72,23 +80,22 @@ public static KerberosAuthenticationToken extractToken(final ThreadContext conte @Override public String principal() { - return UNAUTHENTICATED_PRINCIPAL_NAME; + return ""; } @Override public Object credentials() { - return decodedTicket; + return base64DecodedToken; } @Override public void clearCredentials() { - Arrays.fill(decodedTicket, (byte) 0); - this.decodedTicket = null; + Arrays.fill(base64DecodedToken, (byte) 0); } @Override public int hashCode() { - return Objects.hash(decodedTicket); + return Objects.hash(base64DecodedToken); } @Override 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 78ed32fc8d196..45994aa852b2f 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 @@ -10,49 +10,44 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.ESLoggerFactory; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.xpack.core.security.authc.RealmSettings; -import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; 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 org.ietf.jgss.Oid; -import java.nio.file.Files; import java.nio.file.Path; 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.HashSet; import java.util.Map; -import java.util.Set; import javax.security.auth.Subject; -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; /** - * Responsible for validating Kerberos ticket
- * Performs service login using keytab, supports multiple principals in keytab. + * 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 { - public static final Oid SPNEGO_OID = getSpnegoOid(); +final 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) { - ExceptionsHelper.convertToRuntime(gsse); + throw ExceptionsHelper.convertToRuntime(gsse); } return oid; } @@ -63,12 +58,20 @@ private static Oid getSpnegoOid() { private static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule"; /** - * Validates client kerberos ticket. + * 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 servicePrincipalName Service principal name - * @param nameType {@link GSSName} type depending on GSS API principal entity * @param decodedToken base64 decoded kerberos ticket bytes - * @param config {@link RealmConfig} + * @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}. @@ -77,77 +80,94 @@ private static Oid getSpnegoOid() { * @throws GSSException thrown when GSS Context negotiation fails * {@link GSSException} */ - public Tuple validateTicket(final String servicePrincipalName, Oid nameType, final byte[] decodedToken, - final RealmConfig config) throws LoginException, GSSException { + 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 { - Path keyTabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); - if (Files.exists(keyTabPath) == false) { - throw new IllegalArgumentException("configured service key tab file does not exist for " - + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH)); - } - loginContext = serviceLogin(servicePrincipalName, keyTabPath.toString(), config.settings()); - GSSCredential serviceCreds = createCredentials(servicePrincipalName, nameType, gssManager, loginContext); + loginContext = serviceLogin(keyTabPath.toString(), krbDebug); + GSSCredential serviceCreds = createCredentials(gssManager, loginContext.getSubject()); gssContext = gssManager.createContext(serviceCreds); - final byte[] outToken = acceptSecContext(decodedToken, gssContext, loginContext); - - String base64OutToken = null; - if (outToken != null && outToken.length > 0) { - base64OutToken = Base64.getEncoder().encodeToString(outToken); - } + final String base64OutToken = base64Encode(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.getException() instanceof LoginException) { + if (pve.getCause() instanceof LoginException) { throw (LoginException) pve.getCause(); } - if (pve.getException() instanceof GSSException) { + if (pve.getCause() instanceof GSSException) { throw (GSSException) pve.getCause(); } throw ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(pve)); } finally { - privilegedDisposeNoThrow(loginContext); - privilegedCloseNoThrow(gssContext); + privilegedLogoutNoThrow(loginContext); + privilegedDisposeNoThrow(gssContext); + } + } + + private String base64Encode(final byte[] outToken) { + if (outToken != null && outToken.length > 0) { + return Base64.getEncoder().encodeToString(outToken); } + return null; } - private static byte[] acceptSecContext(final byte[] base64Ticket, GSSContext gssContext, LoginContext loginContext) + /** + * 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 { - final GSSContext finalGSSContext = gssContext; // process token with gss context - return doAsWrapper(loginContext.getSubject(), - (PrivilegedExceptionAction) () -> finalGSSContext.acceptSecContext(base64Ticket, 0, base64Ticket.length)); + return doAsWrapper(subject, + (PrivilegedExceptionAction) () -> gssContext.acceptSecContext(base64decodedTicket, 0, base64decodedTicket.length)); } - private static GSSCredential createCredentials(final String servicePrincipalName, Oid nameType, final GSSManager gssManager, - LoginContext loginContext) throws GSSException, PrivilegedActionException { - final GSSName gssServicePrincipalName; - if (servicePrincipalName.equals("*") == false) { - gssServicePrincipalName = gssManager.createName(servicePrincipalName, nameType); - } else { - gssServicePrincipalName = null; - } - return doAsWrapper(loginContext.getSubject(), (PrivilegedExceptionAction) () -> gssManager - .createCredential(gssServicePrincipalName, GSSCredential.DEFAULT_LIFETIME, SPNEGO_OID, GSSCredential.ACCEPT_ONLY)); + /** + * 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 + * Privileged Wrapper that invokes action with Subject.doAs to perform work as + * given subject. * - * @param subject {@link Subject} + * @param subject {@link Subject} to be used for this work * @param action {@link PrivilegedExceptionAction} action for performing inside * Subject.doAs - * @return + * @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) { - throw (PrivilegedActionException) pae.getException(); + if (pae.getCause() instanceof PrivilegedActionException) { + throw (PrivilegedActionException) pae.getCause(); + } + throw pae; } } @@ -155,9 +175,9 @@ private static T doAsWrapper(final Subject subject, final PrivilegedExceptio * Privileged wrapper for closing GSSContext, does not throw exceptions but logs * them as warning. * - * @param gssContext + * @param gssContext GSSContext to be disposed. */ - private static void privilegedCloseNoThrow(final GSSContext gssContext) { + private static void privilegedDisposeNoThrow(final GSSContext gssContext) { if (gssContext != null) { try { AccessController.doPrivileged((PrivilegedExceptionAction) () -> { @@ -165,10 +185,8 @@ private static void privilegedCloseNoThrow(final GSSContext gssContext) { return null; }); } catch (PrivilegedActionException e) { - RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); - LOGGER.debug("Could not dispose GSS Context", rte); + LOGGER.debug("Could not dispose GSS Context", (Exception) ExceptionsHelper.unwrapCause(e)); } - return; } } @@ -176,9 +194,9 @@ private static void privilegedCloseNoThrow(final GSSContext gssContext) { * Privileged wrapper for closing LoginContext, does not throw exceptions but * logs them as warning. * - * @param loginContext + * @param loginContext LoginContext to be closed */ - private static void privilegedDisposeNoThrow(final LoginContext loginContext) { + private static void privilegedLogoutNoThrow(final LoginContext loginContext) { if (loginContext != null) { try { AccessController.doPrivileged((PrivilegedExceptionAction) () -> { @@ -186,33 +204,25 @@ private static void privilegedDisposeNoThrow(final LoginContext loginContext) { return null; }); } catch (PrivilegedActionException e) { - RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); - LOGGER.debug("Could not close LoginContext", rte); + LOGGER.debug("Could not close LoginContext", (Exception) ExceptionsHelper.unwrapCause(e)); } } } /** - * Performs authentication using provided principal name and keytab + * Performs authentication using provided keytab * - * @param principal Principal name * @param keytabFilePath Keytab file path - * @param settings {@link Settings} + * @param krbDebug if {@code true} enables jaas krb5 login module debug logs.. * @return authenticated {@link LoginContext} instance. Note: This needs to be - * closed {@link LoginContext#logout()} after usage. - * @throws PrivilegedActionException + * closed using {@link LoginContext#logout()} after usage. + * @throws PrivilegedActionException when privileged action threw exception */ - private static LoginContext serviceLogin(final String principal, final String keytabFilePath, final Settings settings) + private static LoginContext serviceLogin(final String keytabFilePath, final boolean krbDebug) throws PrivilegedActionException { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - final Set principals = new HashSet<>(); - if (principal.equals("*") == false) { - principals.add(new KerberosPrincipal(principal)); - } - - final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); - - final Configuration conf = new KeytabJaasConf(principal, keytabFilePath, settings); + 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; @@ -220,34 +230,41 @@ private static LoginContext serviceLogin(final String principal, final String ke } /** - * Instead of jaas.conf, this requires refresh of {@link Configuration}. + * 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 principal; private final String keytabFilePath; - private final Settings settings; + private final boolean krbDebug; - KeytabJaasConf(final String principal, final String keytabFilePath, final Settings settings) { - this.principal = principal; + KeytabJaasConf(final String keytabFilePath, final boolean krbDebug) { this.keytabFilePath = keytabFilePath; - this.settings = settings; + this.krbDebug = krbDebug; } @Override public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { final Map options = new HashMap<>(); options.put("keyTab", keytabFilePath); - options.put("principal", principal); + /* + * 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", KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings).toString()); + options.put("debug", Boolean.toString(krbDebug)); - return new AppConfigurationEntry[] { - new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; + return new AppConfigurationEntry[] { new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Collections.unmodifiableMap(options)) }; } } 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 index db26bb8aac8b1..19c7749b8d6ba 100644 --- 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 @@ -7,100 +7,71 @@ package org.elasticsearch.xpack.security.authc.kerberos; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; -import org.elasticsearch.xpack.security.authc.kerberos.support.KerberosTestCase; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.rules.ExpectedException; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; +import java.util.Arrays; import java.util.Base64; -import java.util.List; + +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.notNullValue; public class KerberosAuthenticationTokenTests extends ESTestCase { private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; - private ThreadContext threadContext; - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Before - public void setup() throws IOException { - final Path dir = createTempDir(); - final Path ktab = KerberosTestCase.writeKeyTab(dir, "http.keytab", null); - final Settings settings = KerberosTestCase.buildKerberosRealmSettings(ktab.toString()); - threadContext = new ThreadContext(settings); - } - - @After - public void cleanup() throws IOException { - threadContext.close(); - threadContext = null; - } - 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 : "negotiate "; - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, negotiate + base64Token); + final String authzHeader = negotiate + base64Token; - final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(authzHeader); assertNotNull(kerbAuthnToken); assertEquals(UNAUTHENTICATED_PRINCIPAL_NAME, kerbAuthnToken.principal()); assertTrue(kerbAuthnToken.credentials() instanceof byte[]); assertArrayEquals(Base64.getDecoder().decode(base64Token), (byte[]) kerbAuthnToken.credentials()); } - public void testExtractTokenForInvalidAuthorizationHeaderThrowsException() throws IOException { - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER); - - thrown.expect(ElasticsearchSecurityException.class); - thrown.expectMessage( - Matchers.equalTo("invalid negotiate authentication header value, expected base64 encoded token but value is empty")); - thrown.expect(new ESEMatcher()); - KerberosAuthenticationToken.extractToken(threadContext); - fail("Expected exception not thrown"); + public void testExtractTokenForInvalidNegotiateAuthorizationHeaderThrowsException() throws IOException { + final String header = + randomFrom(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER, "negotiate", "negotiate ", "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"; - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, - KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + notBase64Token); - - thrown.expect(ElasticsearchSecurityException.class); - thrown.expectMessage( - Matchers.equalTo("invalid negotiate authentication header value, could not decode base64 token " + notBase64Token)); - thrown.expect(new ESEMatcher()); - KerberosAuthenticationToken.extractToken(threadContext); - fail("Expected exception not thrown"); - } - public void testExtractTokenForNoAuthorizationHeaderShouldReturnNull() throws IOException { - final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); - assertNull(kerbAuthnToken); + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + notBase64Token)); + assertThat(e.getMessage(), + equalTo("invalid negotiate authentication header value, could not decode base64 token " + notBase64Token)); + assertContainsAuthenticateHeader(e); } - public void testExtractTokenForBasicAuthorizationHeaderShouldReturnNull() throws IOException { - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, "Basic "); - final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + public void testExtractTokenForInvalidAuthorizationHeaderShouldReturnNull() throws IOException { + final String header = randomFrom(Arrays.asList(" Negotiate", "Basic ", " Custom ", null)); + final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(header); assertNull(kerbAuthnToken); } public void testKerberoAuthenticationTokenClearCredentials() { - final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); - threadContext.putHeader(KerberosAuthenticationToken.AUTH_HEADER, KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); - final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(threadContext); + byte[] inputBytes = randomByteArrayOfLength(5); + final String base64Token = Base64.getEncoder().encodeToString(inputBytes); + final KerberosAuthenticationToken kerbAuthnToken = + KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); kerbAuthnToken.clearCredentials(); - assertNull(kerbAuthnToken.credentials()); + Arrays.fill(inputBytes, (byte) 0); + assertArrayEquals(inputBytes, (byte[]) kerbAuthnToken.credentials()); } public void testEqualsHashCode() { @@ -127,21 +98,11 @@ private static KerberosAuthenticationToken mutateTestItem(KerberosAuthentication } } - private static class ESEMatcher extends BaseMatcher { - @Override - public boolean matches(Object item) { - if (item instanceof ElasticsearchSecurityException) { - List authHeaderValue = - ((ElasticsearchSecurityException) item).getHeader(KerberosAuthenticationToken.WWW_AUTHENTICATE); - if (authHeaderValue.size() == 1 && KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER.contains(authHeaderValue.get(0))) { - return true; - } - } - return false; - } - - @Override - public void describeTo(Description description) { - } + 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_AUTH_HEADER.trim())); } } 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 b0a6fea449165..1542d46182042 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 @@ -24,14 +24,13 @@ public void testKerberosRealmSettings() throws IOException { if (Files.exists(configDir) == false) { configDir = Files.createDirectory(configDir); } - KerberosTestCase.writeKeyTab(dir, "config" + dir.getFileSystem().getSeparator() + "http.keytab", null); + 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("config" + dir.getFileSystem().getSeparator() + "http.keytab", - maxUsers, cacheTTL, true); + final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keyTabPathConfig, maxUsers, cacheTTL, true); - assertEquals("config" + dir.getFileSystem().getSeparator() + "http.keytab", - KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); + assertEquals(keyTabPathConfig, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); assertEquals(TimeValue.parseTimeValue(cacheTTL, KerberosRealmSettings.CACHE_TTL_SETTING.getKey()), KerberosRealmSettings.CACHE_TTL_SETTING.get(settings)); assertEquals(maxUsers, KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(settings)); 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 4f0faf571124c..0c5b2c1bf9daa 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 @@ -6,14 +6,19 @@ 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.elasticsearch.xpack.security.authc.saml.SamlTestCase; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import java.io.BufferedWriter; import java.io.IOException; @@ -24,23 +29,64 @@ 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 + * 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 = new ArrayList<>(); - protected List clientUserNames = new ArrayList<>(); + 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 has problem due to handling of GeneralizedTime in SimpleKdcServer + // For more look at : org.apache.kerby.asn1.type.Asn1GeneralizedTime#toBytes() + unsupportedLocaleLanguages.add("ar"); + } + + @BeforeClass + public static void setupSaml() throws Exception { + Logger logger = Loggers.getLogger(SamlTestCase.class); + if (isLocaleUnsupported()) { + 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 startMiniKdc() throws Exception { @@ -51,12 +97,12 @@ public void startMiniKdc() throws Exception { simpleKdcLdapServer = new SimpleKdcLdapServer(workDir, "com", "example", kdcLdiff); // Create SPNs and UPNs - serviceUserNames.clear(); + 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.clear(); + clientUserNames = new ArrayList<>(); Randomness.get().ints(randomIntBetween(1, 6)).forEach((i) -> { String clientUserName = "client-" + randomAlphaOfLength(8); clientUserNames.add(clientUserName); @@ -74,16 +120,37 @@ 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(); } @@ -94,25 +161,47 @@ protected String principalName(final String user) { * @param subject {@link Subject} * @param action {@link PrivilegedExceptionAction} action for performing inside * Subject.doAs - * @return + * @return Type of value as returned by PrivilegedAction * @throws PrivilegedActionException */ - public static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction action) throws PrivilegedActionException { + static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction action) throws PrivilegedActionException { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); } - public static Path writeKeyTab(final Path dir, final String name, final String content) throws IOException { - final Path path = dir.resolve(name); - try (BufferedWriter bufferedWriter = Files.newBufferedWriter(path, StandardCharsets.US_ASCII)) { + /** + * Write content to keytab provided. + * + * @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 path; + 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) 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 c5aab56eb1f03..c18b5c070f71a 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 @@ -8,18 +8,10 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matchers; import org.ietf.jgss.GSSException; -import org.ietf.jgss.GSSName; -import org.junit.Rule; -import org.junit.rules.ExpectedException; import java.io.IOException; import java.nio.file.Path; @@ -31,105 +23,73 @@ public class KerberosTicketValidatorTests extends KerberosTestCase { private KerberosTicketValidator kerberosTicketValidator = new KerberosTicketValidator(); - @Rule - public ExpectedException thrown = ExpectedException.none(); public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Exception { createPrincipalKeyTab(workDir, "differentServer"); // Client login and init token preparation final String clientUserName = randomFrom(clientUserNames); - final SpnegoClient spnegoClient = - new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer")); - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); - - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); + try (SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer"));) { + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + 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)); + assertEquals(GSSException.FAILURE, gssException.getMajor()); + } } public void testInvalidKerbTicketFailsValidation() throws Exception { final String base64KerbToken = Base64.getEncoder().encodeToString(randomByteArrayOfLength(5)); - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - thrown.expect(new GSSExceptionMatcher(GSSException.DEFECTIVE_TOKEN)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); - } - - public void testWhenKeyTabDoesNotExistFailsValidation() throws LoginException, GSSException { - final String base64KerbToken = Base64.getEncoder().encodeToString(randomByteArrayOfLength(5)); - settings = buildKerberosRealmSettings("random-non-existing.keytab".toString()); - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage(Matchers.equalTo("configured service key tab file does not exist for " - + RealmSettings.getFullSettingKey(config, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH))); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); + 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)); + assertEquals(GSSException.DEFECTIVE_TOKEN, gssException.getMajor()); } public void testWhenKeyTabWithInvalidContentFailsValidation() throws LoginException, GSSException, IOException, PrivilegedActionException { // Client login and init token preparation final String clientUserName = randomFrom(clientUserNames); - final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), - principalName(randomFrom(serviceUserNames))); - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); - - final Path ktabPath = writeKeyTab(workDir, "invalid.keytab", "not - a - valid - key - tab"); - settings = buildKerberosRealmSettings(ktabPath.toString()); - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - thrown.expect(new GSSExceptionMatcher(GSSException.FAILURE)); - kerberosTicketValidator.validateTicket("*", GSSName.NT_USER_NAME, Base64.getDecoder().decode(base64KerbToken), config); + try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames)));) { + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + 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)); + assertEquals(GSSException.FAILURE, gssException.getMajor()); + } } public void testValidKebrerosTicket() throws PrivilegedActionException, GSSException, LoginException { // Client login and init token preparation final String clientUserName = randomFrom(clientUserNames); - final SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), - principalName(randomFrom(serviceUserNames))); - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); - - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - final Tuple userNameOutToken = kerberosTicketValidator.validateTicket("*", GSSName.NT_HOSTBASED_SERVICE, - Base64.getDecoder().decode(base64KerbToken), config); - assertNotNull(userNameOutToken); - assertEquals(principalName(clientUserName), userNameOutToken.v1()); - assertNotNull(userNameOutToken.v2()); - - spnegoClient.handleResponse(userNameOutToken.v2()); - assertTrue(spnegoClient.isEstablished()); - spnegoClient.close(); - } - - class GSSExceptionMatcher extends BaseMatcher { - private int expectedErrorCode; - - GSSExceptionMatcher(int expectedErrorCode) { - this.expectedErrorCode = expectedErrorCode; - } - - @Override - public boolean matches(Object item) { - if (item instanceof GSSException) { - GSSException gssException = (GSSException) item; - if (gssException.getMajor() == expectedErrorCode) { - if (gssException.getMajorString().equals(new GSSException(expectedErrorCode).getMajorString())) { - return true; - } - } - } - return false; - } - - @Override - public void describeTo(Description description) { + try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), + principalName(randomFrom(serviceUserNames)));) { + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + + 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); + assertNotNull(userNameOutToken); + assertEquals(principalName(clientUserName), userNameOutToken.v1()); + assertNotNull(userNameOutToken.v2()); + + spnegoClient.handleResponse(userNameOutToken.v2()); + assertTrue(spnegoClient.isEstablished()); } } + } 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 index 3ceb10e929967..9ed13b44fb778 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/support/SimpleKdcLdapServer.java @@ -34,7 +34,6 @@ * {@link InMemoryDirectoryServer}.
* Starts in memory Ldap server and then uses it as backend for Kdc Server. */ -@SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") public class SimpleKdcLdapServer { private static final Logger logger = Loggers.getLogger(SimpleKdcLdapServer.class); @@ -90,6 +89,7 @@ public Void run() throws Exception { logger.info("SimpleKdcLdapServer started."); } + @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") private void init() throws Exception { // start ldap server createLdapServiceAndStart(); @@ -117,6 +117,7 @@ private void createLdapBackendConf() throws IOException { assert Files.exists(this.workDir.resolve("backend.conf")); } + @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") private void prepareKdcServerAndStart() throws Exception { // transport simpleKdc.setWorkDir(workDir.toFile()); @@ -142,11 +143,12 @@ private void prepareKdcServerAndStart() throws Exception { long maxRenewableLifeTime = simpleKdc.getKdcConfig().getMaximumRenewableLifetime(); simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, 86400000L); simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 604800000L); - logger.info("MINIMUM_TICKET_LIFETIME changed from {} to {}", minimumTicketLifeTime, 86400000L); - logger.info("MAXIMUM_RENEWABLE_LIFETIME changed from {} to {}", maxRenewableLifeTime, 604800000L); + logger.info("MINIMUM_TICKET_LIFETIME changed from {} to {}", minimumTicketLifeTime, + simpleKdc.getKdcConfig().getMinimumTicketLifetime()); + logger.info("MAXIMUM_RENEWABLE_LIFETIME changed from {} to {}", maxRenewableLifeTime, + simpleKdc.getKdcConfig().getMaximumRenewableLifetime()); simpleKdc.init(); simpleKdc.start(); - } public String getRealm() { @@ -181,6 +183,7 @@ public synchronized void createPrincipal(final String principal, final String pa * @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") public synchronized void createPrincipal(final Path keytabFile, final String... principals) throws Exception { simpleKdc.createPrincipals(principals); for (String principal : principals) { @@ -225,4 +228,4 @@ public Void run() throws Exception { 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 index 557b851c7779c..c6339a419ba12 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 @@ -12,17 +12,18 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken; import org.ietf.jgss.GSSException; -import org.ietf.jgss.GSSName; import java.nio.file.Files; +import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.text.ParseException; import java.util.Base64; import javax.security.auth.login.LoginException; @@ -44,31 +45,29 @@ public LDAPConnection run() throws Exception { assertEquals(1, sr.getEntryCount()); } - public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException { + public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException, ParseException { final String serviceUserName = randomFrom(serviceUserNames); // Client login and init token preparation final String clientUserName = randomFrom(clientUserNames); - final SpnegoClient spnegoClient = - new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName)); - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); - final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); + try (SpnegoClient spnegoClient = + new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName));) { + final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + assertNotNull(base64KerbToken); + final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); - // Service Login - final RealmConfig config = new RealmConfig("test-kerb-realm", settings, globalSettings, - TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); - // Handle Authz header which contains base64 token - final Tuple userNameOutToken = new KerberosTicketValidator().validateTicket(principalName(serviceUserName), - GSSName.NT_USER_NAME, (byte[]) kerbAuthnToken.credentials(), config); - assertNotNull(userNameOutToken); - assertEquals(principalName(clientUserName), userNameOutToken.v1()); + // 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); + assertNotNull(userNameOutToken); + assertEquals(principalName(clientUserName), userNameOutToken.v1()); - // Authenticate service on client side. - final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); - assertNull(outToken); - assertTrue(spnegoClient.isEstablished()); - - // Close - spnegoClient.close(); + // Authenticate service on client side. + final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); + assertNull(outToken); + assertTrue(spnegoClient.isEstablished()); + } } } 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 index c43c6f896528b..7d469cc0fdada 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/support/SpnegoClient.java @@ -17,7 +17,6 @@ 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.AccessController; @@ -25,6 +24,8 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Base64; +import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -42,48 +43,46 @@ import javax.security.auth.login.LoginException; /** - * This class is used as a Spnego client and handles spnego interactions.
+ * This class is used as a Spnego client and handles spnego interactions using + * GSS context negotiation.
* Not thread safe */ -public class SpnegoClient { +class SpnegoClient implements AutoCloseable { private static final Logger LOGGER = ESLoggerFactory.getLogger(SpnegoClient.class); - public 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) { - ExceptionsHelper.convertToRuntime(gsse); - } - return oid; - } 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; - private boolean isEstablished; - public SpnegoClient(final String userPrincipalName, final SecureString password, final String servicePrincipalName) + /** + * Creates SpengoClient to interact with given service principal + * + * @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 = getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", "true"); - - LOGGER.info("SpnegoClient with princName : {}", userPrincipalName); + Date date = new Date(); + 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, SPNEGO_OID, GSSCredential.INITIATE_ONLY)); - gssContext = gssManager.createContext(gssServicePrincipalName.canonicalize(SPNEGO_OID), SPNEGO_OID, userCreds, - GSSCredential.DEFAULT_LIFETIME); + 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); - isEstablished = gssContext.isEstablished(); } catch (PrivilegedActionException pve) { LOGGER.error("privileged action exception, with root cause", pve.getException()); throw pve; @@ -92,20 +91,35 @@ public SpnegoClient(final String userPrincipalName, final SecureString password, } } - public String getBase64TicketForSpnegoHeader() throws PrivilegedActionException { + /** + * GSSContext initiator side handling, initiates sec context and returns the + * base64 encoded token to be sent to server. + * + * @return Base64 encoded token + * @throws PrivilegedActionException + */ + String getBase64TicketForSpnegoHeader() 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 + */ public String handleResponse(final String base64Token) throws PrivilegedActionException { - if (isEstablished) { + 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)); - isEstablished = gssContext.isEstablished(); if (outToken == null || outToken.length == 0) { return null; } @@ -127,12 +141,15 @@ public void close() throws LoginException, GSSException, PrivilegedActionExcepti } } - public boolean isEstablished() { - return isEstablished; + /** + * @return {@code true} If the context was established + */ + boolean isEstablished() { + return gssContext.isEstablished(); } /** - * Performs authentication using provided principal name and password + * Performs authentication using provided principal name and password for client * * @param principal Principal name * @param password {@link SecureString} @@ -155,7 +172,12 @@ private static LoginContext loginUsingPassword(final String principal, final Sec } /** - * Instead of jaas.conf, this requires refresh of {@link Configuration}. + * 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; @@ -176,11 +198,14 @@ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { options.put("isInitiator", Boolean.TRUE.toString()); options.put("debug", Boolean.TRUE.toString()); - return new AppConfigurationEntry[] { - new AppConfigurationEntry(SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; + 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; From 9e11414c102d186dfd54f1646787c750ebadaee7 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 8 Jun 2018 14:52:27 +1000 Subject: [PATCH 09/24] [Kerberos] Method rename, close ldapconn after usage --- .../authc/kerberos/support/KerberosTestCase.java | 2 +- .../kerberos/support/SimpleKdcLdapServerTests.java | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) 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 0c5b2c1bf9daa..1239a9ec1b90d 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 @@ -65,7 +65,7 @@ public abstract class KerberosTestCase extends ESTestCase { } @BeforeClass - public static void setupSaml() throws Exception { + public static void setupKerberos() throws Exception { Logger logger = Loggers.getLogger(SamlTestCase.class); if (isLocaleUnsupported()) { logger.warn("Attempting to run Kerberos test on {} locale, but that breaks SimpleKdcServer. Switching to English.", 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 c6339a419ba12..2ebcac9f923e7 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 @@ -33,16 +33,18 @@ 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"))); - LDAPConnection ldapConn = AccessController.doPrivileged(new PrivilegedExceptionAction() { + try (LDAPConnection ldapConn = AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public LDAPConnection run() throws Exception { return new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()); } - }); - assertTrue(ldapConn.isConnected()); - SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(uid=p1)"); - assertEquals(1, sr.getEntryCount()); + });) { + assertTrue(ldapConn.isConnected()); + SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(krb5PrincipalName=p1@EXAMPLE.COM)"); + assertEquals(1, sr.getEntryCount()); + assertEquals("uid=p1,dc=example,dc=com", sr.getSearchEntries().get(0).getDN()); + } } public void testClientServiceMutualAuthentication() throws PrivilegedActionException, GSSException, LoginException, ParseException { From 27440a07153361e67eba2d5f4c6d42f79115d139 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 8 Jun 2018 17:40:16 +1000 Subject: [PATCH 10/24] [Kerberos] reverting change done to make ticket validator final --- .../authc/kerberos/support/KerberosTicketValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 45994aa852b2f..490ac6a60b480 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 @@ -39,7 +39,7 @@ *

* It may respond with token which needs to be communicated with the peer. */ -final class KerberosTicketValidator { +public class KerberosTicketValidator { static final Oid SPNEGO_OID = getSpnegoOid(); private static Oid getSpnegoOid() { @@ -80,7 +80,7 @@ private static Oid getSpnegoOid() { * @throws GSSException thrown when GSS Context negotiation fails * {@link GSSException} */ - Tuple validateTicket(final byte[] decodedToken, final Path keyTabPath, final boolean krbDebug) + public Tuple validateTicket(final byte[] decodedToken, final Path keyTabPath, final boolean krbDebug) throws LoginException, GSSException { final GSSManager gssManager = GSSManager.getInstance(); GSSContext gssContext = null; From ad131497f417e36310f3e5327973a1001d25537e Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Sat, 9 Jun 2018 10:23:30 +1000 Subject: [PATCH 11/24] [Kerberos] Add unsupported languages during test Other languages that do not work with Apache SimpleKdcServer "ja", "th", "hi". Changing the default during testing. --- .../authc/kerberos/support/KerberosTestCase.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 1239a9ec1b90d..d2b810a266c10 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 @@ -59,9 +59,15 @@ public abstract class KerberosTestCase extends ESTestCase { private static Set unsupportedLocaleLanguages; static { unsupportedLocaleLanguages = new HashSet<>(); - // arabic has problem due to handling of GeneralizedTime in SimpleKdcServer - // For more look at : org.apache.kerby.asn1.type.Asn1GeneralizedTime#toBytes() + /* + * 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 From 40e29fab118dec9d9a6c0949c257cee2ea2d69b0 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Wed, 13 Jun 2018 17:16:41 +1000 Subject: [PATCH 12/24] [Kerberos] Fix for certain locale(tr-TR) failures in SimpleKdcServer KdcConfigKey does not specify locale during getProperyKey causing problem loading ldap backend properties. This is workaround as it writes the key in the same locale as expected for that test by using same property key --- .../authc/kerberos/support/SimpleKdcLdapServer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 9ed13b44fb778..4a92fc13d0814 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/support/SimpleKdcLdapServer.java @@ -110,9 +110,10 @@ private void createLdapServiceAndStart() throws Exception { } private void createLdapBackendConf() throws IOException { - String backendConf = - "kdc_identity_backend = 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; + 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")); } From 9fdf1820f15f949b3050ff79bb4e09030b05e6b3 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 14 Jun 2018 13:29:55 +1000 Subject: [PATCH 13/24] [Kerberos] Address review comments from Jay - Removed unwanted trim() and corrected the check for Negotiate - corrected basic authorization check to be case insensitive - Add missing documentation - Upgrade Apache SimpleKdcServer and its dependencies --- .../authc/support/UsernamePasswordToken.java | 13 +++-- x-pack/plugin/security/build.gradle | 50 +++++++++---------- .../kerberos/KerberosAuthenticationToken.java | 31 ++++++------ .../support/KerberosTicketValidator.java | 4 +- .../KerberosAuthenticationTokenTests.java | 25 +++++----- .../kerberos/support/KerberosTestCase.java | 8 ++- .../kerberos/support/SimpleKdcLdapServer.java | 32 +++++------- .../support/SimpleKdcLdapServerTests.java | 12 ++--- .../authc/kerberos/support/SpnegoClient.java | 35 ++++++------- .../support/UsernamePasswordTokenTests.java | 7 +-- 10 files changed, 103 insertions(+), 114 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java index 4fdf32608dd6a..d8e58c29d237b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.security.authc.support; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -20,6 +21,8 @@ public class UsernamePasswordToken implements AuthenticationToken { public static final String BASIC_AUTH_PREFIX = "Basic "; public static final String BASIC_AUTH_HEADER = "Authorization"; + // authorization scheme check is case-insensitive + private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; private final String username; private final SecureString password; @@ -79,15 +82,15 @@ public int hashCode() { public static UsernamePasswordToken extractToken(ThreadContext context) { String authStr = context.getHeader(BASIC_AUTH_HEADER); - if (authStr == null) { - return null; - } - return extractToken(authStr); } private static UsernamePasswordToken extractToken(String headerValue) { - if (headerValue.startsWith(BASIC_AUTH_PREFIX) == false) { + if (Strings.isNullOrEmpty(headerValue)) { + return null; + } + if (headerValue.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, BASIC_AUTH_PREFIX, 0, + BASIC_AUTH_PREFIX.length()) == false) { // the header does not start with 'Basic ' so we cannot use it, but it may be valid for another realm return null; } diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 4032dd270032b..c280a516ef823 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -60,28 +60,28 @@ dependencies { // Test dependencies for Kerberos (MiniKdc) testCompile('commons-io:commons-io:2.5') - testCompile('org.apache.kerby:kerb-simplekdc:1.0.1') - testCompile('org.apache.kerby:kerb-client:1.0.1') - testCompile('org.apache.kerby:kerby-config:1.0.1') - testCompile('org.apache.kerby:kerb-core:1.0.1') - testCompile('org.apache.kerby:kerby-pkix:1.0.1') - testCompile('org.apache.kerby:kerby-asn1:1.0.1') - testCompile('org.apache.kerby:kerby-util:1.0.1') - testCompile('org.apache.kerby:kerb-common:1.0.1') - testCompile('org.apache.kerby:kerb-crypto:1.0.1') - testCompile('org.apache.kerby:kerb-util:1.0.1') - testCompile('org.apache.kerby:token-provider:1.0.1') - testCompile('com.nimbusds:nimbus-jose-jwt:3.10') + 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.0.1') - testCompile('org.apache.kerby:kerb-server:1.0.1') - testCompile('org.apache.kerby:kerb-identity:1.0.1') - testCompile('org.apache.kerby:kerby-xdr:1.0.1') + 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.0.1') - testCompile('org.apache.kerby:ldap-backend:1.0.1') - testCompile('org.apache.kerby:kerb-identity:1.0.1') + 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') @@ -90,12 +90,12 @@ dependencies { 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.16') - testCompile('org.apache.directory.api:api-util:1.0.0') - testCompile('org.apache.directory.api:api-i18n:1.0.0') - testCompile('org.apache.directory.api:api-ldap-model:1.0.0') - testCompile('org.apache.directory.api:api-asn1-api:1.0.0') - testCompile('org.apache.directory.api:api-asn1-ber:1.0.0') + 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') 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 f697963cb6f03..c7dff2da755b5 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 @@ -11,15 +11,13 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; -import java.util.Objects; /** - * This class represents AuthenticationToken for Kerberos authentication using - * SPNEGO mechanism. The token stores base 64 decoded token bytes, extracted - * from the Authorization header with auth scheme 'Negotiate'. + * 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..." @@ -32,8 +30,10 @@ 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_AUTH_HEADER = "Negotiate "; + public static final String NEGOTIATE_SCHEME_NAME = "Negotiate"; + public static final String NEGOTIATE_AUTH_HEADER = NEGOTIATE_SCHEME_NAME + " "; + // authorization scheme check is case-insensitive private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; private final byte[] base64DecodedToken; @@ -57,20 +57,19 @@ public static KerberosAuthenticationToken extractToken(final String authorizatio if (Strings.isNullOrEmpty(authorizationHeader)) { return null; } - - if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER.trim(), 0, - NEGOTIATE_AUTH_HEADER.trim().length()) == false) { + if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER, 0, + NEGOTIATE_AUTH_HEADER.length()) == false) { return null; } - final String base64EncodedToken = authorizationHeader.substring(NEGOTIATE_AUTH_HEADER.trim().length()).trim(); + final String base64EncodedToken = authorizationHeader.substring(NEGOTIATE_AUTH_HEADER.length()).trim(); if (Strings.isEmpty(base64EncodedToken)) { throw unauthorized("invalid negotiate authentication header value, expected base64 encoded token but value is empty", null); } - final byte[] base64Token = base64EncodedToken.getBytes(StandardCharsets.UTF_8); + byte[] decodedKerberosTicket = null; try { - decodedKerberosTicket = Base64.getDecoder().decode(base64Token); + decodedKerberosTicket = Base64.getDecoder().decode(base64EncodedToken); } catch (IllegalArgumentException iae) { throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", iae, base64EncodedToken); } @@ -80,7 +79,7 @@ public static KerberosAuthenticationToken extractToken(final String authorizatio @Override public String principal() { - return ""; + return ""; } @Override @@ -95,7 +94,7 @@ public void clearCredentials() { @Override public int hashCode() { - return Objects.hash(base64DecodedToken); + return Arrays.hashCode(base64DecodedToken); } @Override @@ -107,12 +106,12 @@ public boolean equals(final Object other) { if (getClass() != other.getClass()) return false; final KerberosAuthenticationToken otherKerbToken = (KerberosAuthenticationToken) other; - return Objects.equals(otherKerbToken.credentials(), credentials()); + return Arrays.equals(otherKerbToken.base64DecodedToken, this.base64DecodedToken); } 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_AUTH_HEADER.trim()); + 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 index 490ac6a60b480..eb5190149a2f3 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 @@ -173,7 +173,7 @@ private static T doAsWrapper(final Subject subject, final PrivilegedExceptio /** * Privileged wrapper for closing GSSContext, does not throw exceptions but logs - * them as warning. + * them as debug message. * * @param gssContext GSSContext to be disposed. */ @@ -192,7 +192,7 @@ private static void privilegedDisposeNoThrow(final GSSContext gssContext) { /** * Privileged wrapper for closing LoginContext, does not throw exceptions but - * logs them as warning. + * logs them as debug message. * * @param loginContext LoginContext to be closed */ 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 index 19c7749b8d6ba..3fd8d795d6a77 100644 --- 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 @@ -24,7 +24,7 @@ public class KerberosAuthenticationTokenTests extends ESTestCase { - private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; + private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; public void testExtractTokenForValidAuthorizationHeader() throws IOException { final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); @@ -38,9 +38,13 @@ public void testExtractTokenForValidAuthorizationHeader() throws IOException { assertArrayEquals(Base64.getDecoder().decode(base64Token), (byte[]) kerbAuthnToken.credentials()); } - public void testExtractTokenForInvalidNegotiateAuthorizationHeaderThrowsException() throws IOException { - final String header = - randomFrom(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER, "negotiate", "negotiate ", "Negotiate", "Negotiate "); + 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, "negotiate ", "Negotiate "); final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> KerberosAuthenticationToken.extractToken(header)); assertThat(e.getMessage(), @@ -58,12 +62,6 @@ public void testExtractTokenForNotBase64EncodedTokenThrowsException() throws IOE assertContainsAuthenticateHeader(e); } - public void testExtractTokenForInvalidAuthorizationHeaderShouldReturnNull() throws IOException { - final String header = randomFrom(Arrays.asList(" Negotiate", "Basic ", " Custom ", null)); - final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(header); - assertNull(kerbAuthnToken); - } - public void testKerberoAuthenticationTokenClearCredentials() { byte[] inputBytes = randomByteArrayOfLength(5); final String base64Token = Base64.getEncoder().encodeToString(inputBytes); @@ -80,6 +78,10 @@ public void testEqualsHashCode() { 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); @@ -102,7 +104,6 @@ private static void assertContainsAuthenticateHeader(ElasticsearchSecurityExcept 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_AUTH_HEADER.trim())); + 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/support/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTestCase.java index d2b810a266c10..9a9453a8064bd 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 @@ -14,7 +14,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.elasticsearch.xpack.security.authc.saml.SamlTestCase; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -72,8 +71,8 @@ public abstract class KerberosTestCase extends ESTestCase { @BeforeClass public static void setupKerberos() throws Exception { - Logger logger = Loggers.getLogger(SamlTestCase.class); 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(); @@ -94,8 +93,7 @@ private static boolean isLocaleUnsupported() { } @Before - public void startMiniKdc() throws Exception { - + public void startSimpleKdcLdapServer() throws Exception { workDir = createTempDir(); globalSettings = Settings.builder().put("path.home", workDir).build(); @@ -175,7 +173,7 @@ static T doAsWrapper(final Subject subject, final PrivilegedExceptionAction< } /** - * Write content to keytab provided. + * Write content to provided keytab file. * * @param keytabPath {@link Path} to keytab file. * @param content Content for keytab 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 index 4a92fc13d0814..f388d30336be8 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/support/SimpleKdcLdapServer.java @@ -18,6 +18,7 @@ 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; @@ -28,6 +29,7 @@ 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 @@ -55,7 +57,9 @@ public class SimpleKdcLdapServer { /** * Constructor for SimpleKdcLdapServer, creates instance of Kdc server and ldap - * backend server. Also initializes them with provided configuration. + * 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 @@ -111,9 +115,8 @@ private void createLdapServiceAndStart() throws Exception { 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; + + " = 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")); } @@ -140,14 +143,10 @@ private void prepareKdcServerAndStart() throws Exception { } else { throw new IllegalArgumentException("Need to set transport!"); } - long minimumTicketLifeTime = simpleKdc.getKdcConfig().getMinimumTicketLifetime(); - long maxRenewableLifeTime = simpleKdc.getKdcConfig().getMaximumRenewableLifetime(); - simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, 86400000L); - simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 604800000L); - logger.info("MINIMUM_TICKET_LIFETIME changed from {} to {}", minimumTicketLifeTime, - simpleKdc.getKdcConfig().getMinimumTicketLifetime()); - logger.info("MAXIMUM_RENEWABLE_LIFETIME changed from {} to {}", maxRenewableLifeTime, - simpleKdc.getKdcConfig().getMaximumRenewableLifetime()); + 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(); } @@ -184,7 +183,7 @@ public synchronized void createPrincipal(final String principal, final String pa * @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") + @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) { @@ -213,13 +212,6 @@ public Void run() throws Exception { } } - try { - // Will be fixed in next Kerby version. - Thread.sleep(1000); - } catch (InterruptedException e) { - throw ExceptionsHelper.convertToRuntime(e); - } - if (ldapServer != null) { ldapServer.shutDown(true); } 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 2ebcac9f923e7..ca5c095aec115 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 @@ -16,13 +16,12 @@ 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.AccessController; import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.text.ParseException; import java.util.Base64; @@ -33,13 +32,8 @@ 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 = AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public LDAPConnection run() throws Exception { - return new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()); - } - });) { + try (LDAPConnection ldapConn = + LdapUtils.privilegedConnect(() -> new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()));) { assertTrue(ldapConn.isConnected()); SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(krb5PrincipalName=p1@EXAMPLE.COM)"); assertEquals(1, sr.getEntryCount()); 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 index 7d469cc0fdada..f02bd6314a11c 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/support/SpnegoClient.java @@ -25,9 +25,7 @@ import java.security.PrivilegedExceptionAction; import java.util.Base64; import java.util.Collections; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -43,9 +41,11 @@ import javax.security.auth.login.LoginException; /** - * This class is used as a Spnego client and handles spnego interactions using - * GSS context negotiation.
- * Not thread safe + * 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. */ class SpnegoClient implements AutoCloseable { private static final Logger LOGGER = ESLoggerFactory.getLogger(SpnegoClient.class); @@ -58,7 +58,7 @@ class SpnegoClient implements AutoCloseable { /** * Creates SpengoClient to interact with given service principal - * + * * @param userPrincipalName User principal name for login as client * @param password password for client * @param servicePrincipalName Service principal name with whom this client @@ -70,8 +70,7 @@ class SpnegoClient implements AutoCloseable { throws PrivilegedActionException, GSSException { String oldUseSubjectCredsOnlyFlag = null; try { - oldUseSubjectCredsOnlyFlag = getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", "true"); - Date date = new Date(); + 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); @@ -87,7 +86,7 @@ class SpnegoClient implements AutoCloseable { LOGGER.error("privileged action exception, with root cause", pve.getException()); throw pve; } finally { - getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", oldUseSubjectCredsOnlyFlag); + getAndSetUseSubjectCredsOnlySystemProperty(oldUseSubjectCredsOnlyFlag); } } @@ -113,7 +112,7 @@ String getBase64TicketForSpnegoHeader() throws PrivilegedActionException { * nothing to be sent. * @throws PrivilegedActionException */ - public String handleResponse(final String base64Token) throws PrivilegedActionException { + String handleResponse(final String base64Token) throws PrivilegedActionException { if (gssContext.isEstablished()) { throw new IllegalStateException("GSS Context has already been established"); } @@ -126,6 +125,9 @@ public String handleResponse(final String base64Token) throws PrivilegedActionEx return Base64.getEncoder().encodeToString(outToken); } + /** + * Logout from {@link LoginContext} and disposes {@link GSSContext} + */ public void close() throws LoginException, GSSException, PrivilegedActionException { if (loginContext != null) { AccessController.doPrivileged((PrivilegedExceptionAction) () -> { @@ -159,10 +161,9 @@ boolean isEstablished() { * @throws LoginException */ private static LoginContext loginUsingPassword(final String principal, final SecureString password) throws LoginException { - final Set principals = new HashSet<>(); - principals.add(new KerberosPrincipal(principal)); + final Set principals = Collections.singleton(new KerberosPrincipal(principal)); - final Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + final Subject subject = new Subject(false, principals, Collections.emptySet(), Collections.emptySet()); final Configuration conf = new PasswordJaasConf(principal); final CallbackHandler callback = new KrbCallbackHandler(principal, password); @@ -228,18 +229,18 @@ public void handle(final Callback[] callbacks) throws IOException, UnsupportedCa } } - private static String getAndSetSystemProperty(final String systemProperty, final String value) { + private static String getAndSetUseSubjectCredsOnlySystemProperty(final String value) { String retVal = null; try { retVal = AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override @SuppressForbidden( - reason = "For testing application provides credentials, needs sys prop javax.security.auth.useSubjectCredsOnly") + reason = "For tests where we provide credentials, need to set reset javax.security.auth.useSubjectCredsOnly") public String run() throws Exception { - String oldValue = System.getProperty(systemProperty); + String oldValue = System.getProperty("javax.security.auth.useSubjectCredsOnly"); if (value != null) { - System.setProperty(systemProperty, value); + System.setProperty("javax.security.auth.useSubjectCredsOnly", value); } return oldValue; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java index 57c452798844c..bed629d6c64ce 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java @@ -45,7 +45,8 @@ public void testPutToken() throws Exception { public void testExtractToken() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - String header = "Basic " + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); + String header = + randomFrom("Basic ", "basic ") + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext); assertThat(token, notNullValue()); @@ -54,7 +55,7 @@ public void testExtractToken() throws Exception { } public void testExtractTokenInvalid() throws Exception { - String[] invalidValues = { "Basic ", "Basic f" }; + String[] invalidValues = { "Basic ", "Basic f", "basic "}; for (String value : invalidValues) { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value); @@ -70,7 +71,7 @@ public void testExtractTokenInvalid() throws Exception { public void testHeaderNotMatchingReturnsNull() { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - String header = randomFrom("BasicBroken", "invalid", "Basic"); + String header = randomFrom("Basic", "BasicBroken", "invalid", " basic "); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext); assertThat(extracted, nullValue()); From b8f68e10428c7575c71b14a194166e1c4b7d5fba Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 15 Jun 2018 14:26:54 +1000 Subject: [PATCH 14/24] [Kerberos] Forgot to add one changed file --- .../xpack/security/authc/kerberos/support/SpnegoClient.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index f02bd6314a11c..b8405702b896b 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/support/SpnegoClient.java @@ -45,7 +45,9 @@ * 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. + * 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); From 2db7f0ce69b5fb451e418c4fe3d469078c95a09e Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Wed, 20 Jun 2018 09:29:56 +1000 Subject: [PATCH 15/24] [Kerberos] Address review comment --- .../authc/kerberos/support/SimpleKdcLdapServer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index f388d30336be8..02bc236b8ec3d 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/support/SimpleKdcLdapServer.java @@ -75,7 +75,7 @@ public SimpleKdcLdapServer(final Path workDir, final String orgName, final Strin this.ldiff = ldiff; this.krb5DebugBackupConfigValue = AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override - @SuppressForbidden(reason = "set or clear system property krb5 debug in tests") + @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()); @@ -93,7 +93,7 @@ public Void run() throws Exception { logger.info("SimpleKdcLdapServer started."); } - @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") + @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(); @@ -121,7 +121,7 @@ private void createLdapBackendConf() throws IOException { assert Files.exists(this.workDir.resolve("backend.conf")); } - @SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") + @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()); @@ -200,7 +200,7 @@ public synchronized void stop() throws PrivilegedActionException { AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override - @SuppressForbidden(reason = "set or clear system property krb5 debug in tests") + @SuppressForbidden(reason = "set or clear system property krb5 debug in kerberos tests") public Void run() throws Exception { if (simpleKdc != null) { try { From 5f142f1ca9ad0b8dae1b6920c8bf2579118a7e5e Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Wed, 20 Jun 2018 09:45:40 +1000 Subject: [PATCH 16/24] [Kerberos] Remove extra full stop in comment. --- .../authc/kerberos/support/KerberosTicketValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 eb5190149a2f3..463711300419c 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 @@ -213,7 +213,7 @@ private static void privilegedLogoutNoThrow(final LoginContext loginContext) { * Performs authentication using provided keytab * * @param keytabFilePath Keytab file path - * @param krbDebug if {@code true} enables jaas krb5 login module debug logs.. + * @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 From 6bdc904d8551d4e62c3b22c2a5ae038f583f94df Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Wed, 20 Jun 2018 13:15:29 +1000 Subject: [PATCH 17/24] [Kerberos] Update java doc to close SpnegoClient after usage --- .../security/authc/kerberos/support/SpnegoClient.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index b8405702b896b..d8b6597d11db4 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/support/SpnegoClient.java @@ -59,8 +59,9 @@ class SpnegoClient implements AutoCloseable { private final GSSContext gssContext; /** - * Creates SpengoClient to interact with given service principal - * + * Creates SpengoClient to interact with given service principal
+ * Use {@link #close()} to release and dispose {@link LoginContext} and + * {@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 @@ -128,7 +129,8 @@ String handleResponse(final String base64Token) throws PrivilegedActionException } /** - * Logout from {@link LoginContext} and disposes {@link GSSContext} + * Spnego Client after usage needs to be closed in order to logout from + * {@link LoginContext} and disposes {@link GSSContext} */ public void close() throws LoginException, GSSException, PrivilegedActionException { if (loginContext != null) { From fd02cbedf8b3df3bc5e1b2414e29d699e78425a2 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 21 Jun 2018 07:18:12 +1000 Subject: [PATCH 18/24] [Kerberos] Address review comments. - Removed basic authentication bug fix, will create a bug and raise PR against that. - Corrected variable name - Corrected statements in comments and javadoc - Readability in tests --- .../authc/support/UsernamePasswordToken.java | 13 ++++----- .../kerberos/KerberosAuthenticationToken.java | 14 +++++----- .../support/KerberosTicketValidator.java | 4 +-- .../KerberosAuthenticationTokenTests.java | 11 ++++---- .../kerberos/KerberosRealmSettingsTests.java | 13 +++++---- .../support/KerberosTicketValidatorTests.java | 28 +++++++++++-------- .../support/SimpleKdcLdapServerTests.java | 22 +++++++++------ .../authc/kerberos/support/SpnegoClient.java | 2 +- .../support/UsernamePasswordTokenTests.java | 4 +-- 9 files changed, 62 insertions(+), 49 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java index d8e58c29d237b..4fdf32608dd6a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.authc.support; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -21,8 +20,6 @@ public class UsernamePasswordToken implements AuthenticationToken { public static final String BASIC_AUTH_PREFIX = "Basic "; public static final String BASIC_AUTH_HEADER = "Authorization"; - // authorization scheme check is case-insensitive - private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; private final String username; private final SecureString password; @@ -82,15 +79,15 @@ public int hashCode() { public static UsernamePasswordToken extractToken(ThreadContext context) { String authStr = context.getHeader(BASIC_AUTH_HEADER); + if (authStr == null) { + return null; + } + return extractToken(authStr); } private static UsernamePasswordToken extractToken(String headerValue) { - if (Strings.isNullOrEmpty(headerValue)) { - return null; - } - if (headerValue.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, BASIC_AUTH_PREFIX, 0, - BASIC_AUTH_PREFIX.length()) == false) { + if (headerValue.startsWith(BASIC_AUTH_PREFIX) == false) { // the header does not start with 'Basic ' so we cannot use it, but it may be valid for another realm return null; } 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 c7dff2da755b5..c15bcc5ee43a7 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 @@ -31,7 +31,7 @@ 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 = NEGOTIATE_SCHEME_NAME + " "; + 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; @@ -44,12 +44,12 @@ public KerberosAuthenticationToken(final byte[] base64DecodedToken) { /** * Extract token from authorization header and if it is valid - * {@link #NEGOTIATE_AUTH_HEADER} then returns + * {@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 not an - * {@link #NEGOTIATE_AUTH_HEADER} else returns valid + * @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. */ @@ -57,12 +57,12 @@ public static KerberosAuthenticationToken extractToken(final String authorizatio if (Strings.isNullOrEmpty(authorizationHeader)) { return null; } - if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER, 0, - NEGOTIATE_AUTH_HEADER.length()) == false) { + 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.length()).trim(); + 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); } 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 463711300419c..e257101e3a6bd 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 @@ -173,7 +173,7 @@ private static T doAsWrapper(final Subject subject, final PrivilegedExceptio /** * Privileged wrapper for closing GSSContext, does not throw exceptions but logs - * them as debug message. + * them as a debug message. * * @param gssContext GSSContext to be disposed. */ @@ -192,7 +192,7 @@ private static void privilegedDisposeNoThrow(final GSSContext gssContext) { /** * Privileged wrapper for closing LoginContext, does not throw exceptions but - * logs them as debug message. + * logs them as a debug message. * * @param loginContext LoginContext to be closed */ 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 index 3fd8d795d6a77..31fb7f27d3ffe 100644 --- 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 @@ -20,6 +20,7 @@ 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 { @@ -28,13 +29,13 @@ public class KerberosAuthenticationTokenTests extends ESTestCase { 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 : "negotiate "; + 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()); - assertTrue(kerbAuthnToken.credentials() instanceof byte[]); + assertThat(kerbAuthnToken.credentials(), instanceOf((byte[].class))); assertArrayEquals(Base64.getDecoder().decode(base64Token), (byte[]) kerbAuthnToken.credentials()); } @@ -44,7 +45,7 @@ public void testExtractTokenForInvalidNegotiateAuthorizationHeaderShouldReturnNu } public void testExtractTokenForNegotiateAuthorizationHeaderWithNoTokenShouldThrowException() throws IOException { - final String header = randomFrom(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER, "negotiate ", "Negotiate "); + final String header = randomFrom(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX, "negotiate ", "Negotiate "); final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> KerberosAuthenticationToken.extractToken(header)); assertThat(e.getMessage(), @@ -56,7 +57,7 @@ public void testExtractTokenForNotBase64EncodedTokenThrowsException() throws IOE final String notBase64Token = "[B@6499375d"; final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + notBase64Token)); + () -> 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); @@ -66,7 +67,7 @@ public void testKerberoAuthenticationTokenClearCredentials() { byte[] inputBytes = randomByteArrayOfLength(5); final String base64Token = Base64.getEncoder().encodeToString(inputBytes); final KerberosAuthenticationToken kerbAuthnToken = - KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER + base64Token); + KerberosAuthenticationToken.extractToken(KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX + base64Token); kerbAuthnToken.clearCredentials(); Arrays.fill(inputBytes, (byte) 0); assertArrayEquals(inputBytes, (byte[]) kerbAuthnToken.credentials()); 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 1542d46182042..6dc2b4f2af868 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 @@ -16,6 +16,9 @@ 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 { @@ -30,11 +33,11 @@ public void testKerberosRealmSettings() throws IOException { final String cacheTTL = randomLongBetween(10L, 100L) + "m"; final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keyTabPathConfig, maxUsers, cacheTTL, true); - assertEquals(keyTabPathConfig, KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); - assertEquals(TimeValue.parseTimeValue(cacheTTL, KerberosRealmSettings.CACHE_TTL_SETTING.getKey()), - KerberosRealmSettings.CACHE_TTL_SETTING.get(settings)); - assertEquals(maxUsers, KerberosRealmSettings.CACHE_MAX_USERS_SETTING.get(settings)); - assertEquals(Boolean.TRUE, KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(settings)); + 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/KerberosTicketValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/KerberosTicketValidatorTests.java index c18b5c070f71a..1e5578617f2df 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 @@ -20,6 +20,11 @@ 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(); @@ -32,13 +37,13 @@ public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Ex try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer"));) { final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); + 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)); - assertEquals(GSSException.FAILURE, gssException.getMajor()); + assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); } } @@ -49,7 +54,7 @@ public void testInvalidKerbTicketFailsValidation() throws Exception { 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)); - assertEquals(GSSException.DEFECTIVE_TOKEN, gssException.getMajor()); + assertThat(gssException.getMajor(), equalTo(GSSException.DEFECTIVE_TOKEN)); } public void testWhenKeyTabWithInvalidContentFailsValidation() @@ -59,7 +64,7 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames)));) { final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); + assertThat(base64KerbToken, is(notNullValue())); final Path ktabPath = writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); settings = buildKerberosRealmSettings(ktabPath.toString()); @@ -67,7 +72,7 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() 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)); - assertEquals(GSSException.FAILURE, gssException.getMajor()); + assertThat(gssException.getMajor(), equalTo(GSSException.FAILURE)); } } @@ -77,18 +82,19 @@ public void testValidKebrerosTicket() throws PrivilegedActionException, GSSExcep try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames)));) { final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); + 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); - assertNotNull(userNameOutToken); - assertEquals(principalName(clientUserName), userNameOutToken.v1()); - assertNotNull(userNameOutToken.v2()); + assertThat(userNameOutToken, is(notNullValue())); + assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); + assertThat(userNameOutToken.v2(), is(notNullValue())); - spnegoClient.handleResponse(userNameOutToken.v2()); - assertTrue(spnegoClient.isEstablished()); + 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/SimpleKdcLdapServerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/support/SimpleKdcLdapServerTests.java index ca5c095aec115..06796a6a33421 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 @@ -27,6 +27,12 @@ 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 { @@ -34,10 +40,10 @@ public void testPrincipalCreationAndSearchOnLdap() throws Exception { assertTrue(Files.exists(workDir.resolve("p1p2.keytab"))); try (LDAPConnection ldapConn = LdapUtils.privilegedConnect(() -> new LDAPConnection("localhost", simpleKdcLdapServer.getLdapListenPort()));) { - assertTrue(ldapConn.isConnected()); + assertThat(ldapConn.isConnected(), is(true)); SearchResult sr = ldapConn.search("dc=example,dc=com", SearchScope.SUB, "(krb5PrincipalName=p1@EXAMPLE.COM)"); - assertEquals(1, sr.getEntryCount()); - assertEquals("uid=p1,dc=example,dc=com", sr.getSearchEntries().get(0).getDN()); + assertThat(sr.getSearchEntries(), hasSize(1)); + assertThat(sr.getSearchEntries().get(0).getDN(), equalTo("uid=p1,dc=example,dc=com")); } } @@ -48,7 +54,7 @@ public void testClientServiceMutualAuthentication() throws PrivilegedActionExcep try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName));) { final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); - assertNotNull(base64KerbToken); + assertThat(base64KerbToken, is(notNullValue())); final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); // Service Login @@ -57,13 +63,13 @@ public void testClientServiceMutualAuthentication() throws PrivilegedActionExcep // Handle Authz header which contains base64 token final Tuple userNameOutToken = new KerberosTicketValidator().validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, true); - assertNotNull(userNameOutToken); - assertEquals(principalName(clientUserName), userNameOutToken.v1()); + assertThat(userNameOutToken, is(notNullValue())); + assertThat(userNameOutToken.v1(), equalTo(principalName(clientUserName))); // Authenticate service on client side. final String outToken = spnegoClient.handleResponse(userNameOutToken.v2()); - assertNull(outToken); - assertTrue(spnegoClient.isEstablished()); + 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 index d8b6597d11db4..f9d14c1a84049 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/support/SpnegoClient.java @@ -240,7 +240,7 @@ private static String getAndSetUseSubjectCredsOnlySystemProperty(final String va @Override @SuppressForbidden( - reason = "For tests where we provide credentials, need to set reset javax.security.auth.useSubjectCredsOnly") + 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) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java index bed629d6c64ce..25a95d2a18132 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java @@ -55,7 +55,7 @@ public void testExtractToken() throws Exception { } public void testExtractTokenInvalid() throws Exception { - String[] invalidValues = { "Basic ", "Basic f", "basic "}; + String[] invalidValues = { "Basic ", "Basic f" }; for (String value : invalidValues) { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value); @@ -71,7 +71,7 @@ public void testExtractTokenInvalid() throws Exception { public void testHeaderNotMatchingReturnsNull() { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - String header = randomFrom("Basic", "BasicBroken", "invalid", " basic "); + String header = randomFrom("BasicBroken", "invalid", "Basic"); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); UsernamePasswordToken extracted = UsernamePasswordToken.extractToken(threadContext); assertThat(extracted, nullValue()); From 72d33f076157754f09ea251ef4284e6ce8582d58 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 21 Jun 2018 09:01:42 +1000 Subject: [PATCH 19/24] [Kerberos] Revert UsernamePasswordTokenTests case insensitive handling of basic auth header will be addressed in separate PR. --- .../security/authc/support/UsernamePasswordTokenTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java index 25a95d2a18132..4bfdcff778756 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java @@ -45,8 +45,7 @@ public void testPutToken() throws Exception { public void testExtractToken() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - String header = - randomFrom("Basic ", "basic ") + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); + String header = randomFrom("Basic ") + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext); assertThat(token, notNullValue()); From b256f11052555e87be09ec500d5d2821dfeec549 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 21 Jun 2018 09:04:52 +1000 Subject: [PATCH 20/24] [Kerberos] Revert exactly as it was --- .../security/authc/support/UsernamePasswordTokenTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java index 4bfdcff778756..57c452798844c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/UsernamePasswordTokenTests.java @@ -45,7 +45,7 @@ public void testPutToken() throws Exception { public void testExtractToken() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - String header = randomFrom("Basic ") + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); + String header = "Basic " + Base64.getEncoder().encodeToString("user1:test123".getBytes(StandardCharsets.UTF_8)); threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header); UsernamePasswordToken token = UsernamePasswordToken.extractToken(threadContext); assertThat(token, notNullValue()); From 1c83bfb92409b4539c29bafa68118b062dcaf138 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 22 Jun 2018 11:03:34 +1000 Subject: [PATCH 21/24] [Kerberos] Correct javadoc --- .../xpack/security/authc/kerberos/support/SpnegoClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index f9d14c1a84049..4195194c7ed5d 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/support/SpnegoClient.java @@ -60,7 +60,7 @@ class SpnegoClient implements AutoCloseable { /** * Creates SpengoClient to interact with given service principal
- * Use {@link #close()} to release and dispose {@link LoginContext} and + * 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 @@ -130,7 +130,7 @@ String handleResponse(final String base64Token) throws PrivilegedActionException /** * Spnego Client after usage needs to be closed in order to logout from - * {@link LoginContext} and disposes {@link GSSContext} + * {@link LoginContext} and dispose {@link GSSContext} */ public void close() throws LoginException, GSSException, PrivilegedActionException { if (loginContext != null) { From 28e7fe4dd1d13bd7473e7f9e371b61d9bd6cc960 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 22 Jun 2018 14:45:54 +1000 Subject: [PATCH 22/24] [Kerberos] Address review comments from Tim --- .../authc/kerberos/KerberosRealmSettings.java | 9 ++------- .../kerberos/KerberosAuthenticationToken.java | 16 ++++++++-------- .../support/KerberosTicketValidator.java | 12 ++++++------ .../kerberos/KerberosRealmSettingsTests.java | 8 ++++---- 4 files changed, 20 insertions(+), 25 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 85fd2ecbf3e43..a3dfb501928ee 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 @@ -9,8 +9,8 @@ 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.HashSet; import java.util.Set; /** @@ -42,11 +42,6 @@ private KerberosRealmSettings() { * @return the valid set of {@link Setting}s for a {@value #TYPE} realm */ public static Set> getSettings() { - Set> settings = new HashSet<>(); - settings.add(HTTP_SERVICE_KEYTAB_PATH); - settings.add(CACHE_TTL_SETTING); - settings.add(CACHE_MAX_USERS_SETTING); - settings.add(SETTING_KRB_DEBUG_ENABLE); - return settings; + 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/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 c15bcc5ee43a7..e78624ee687d7 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 @@ -36,10 +36,10 @@ public final class KerberosAuthenticationToken implements AuthenticationToken { // authorization scheme check is case-insensitive private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; - private final byte[] base64DecodedToken; + private final byte[] decodedToken; - public KerberosAuthenticationToken(final byte[] base64DecodedToken) { - this.base64DecodedToken = base64DecodedToken; + public KerberosAuthenticationToken(final byte[] decodedToken) { + this.decodedToken = decodedToken; } /** @@ -79,22 +79,22 @@ public static KerberosAuthenticationToken extractToken(final String authorizatio @Override public String principal() { - return ""; + return ""; } @Override public Object credentials() { - return base64DecodedToken; + return decodedToken; } @Override public void clearCredentials() { - Arrays.fill(base64DecodedToken, (byte) 0); + Arrays.fill(decodedToken, (byte) 0); } @Override public int hashCode() { - return Arrays.hashCode(base64DecodedToken); + return Arrays.hashCode(decodedToken); } @Override @@ -106,7 +106,7 @@ public boolean equals(final Object other) { if (getClass() != other.getClass()) return false; final KerberosAuthenticationToken otherKerbToken = (KerberosAuthenticationToken) other; - return Arrays.equals(otherKerbToken.base64DecodedToken, this.base64DecodedToken); + return Arrays.equals(otherKerbToken.decodedToken, this.decodedToken); } private static ElasticsearchSecurityException unauthorized(final String message, final Throwable cause, final Object... args) { 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 e257101e3a6bd..921dce9ef2812 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 @@ -69,7 +69,7 @@ private static Oid getSpnegoOid() { * negotiation. * * @param decodedToken base64 decoded kerberos ticket bytes - * @param keyTabPath Path to Service key tab file containing credentials for ES + * @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 @@ -80,13 +80,13 @@ private static Oid getSpnegoOid() { * @throws GSSException thrown when GSS Context negotiation fails * {@link GSSException} */ - public Tuple validateTicket(final byte[] decodedToken, final Path keyTabPath, final boolean krbDebug) + 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); + loginContext = serviceLogin(keytabPath.toString(), krbDebug); GSSCredential serviceCreds = createCredentials(gssManager, loginContext.getSubject()); gssContext = gssManager.createContext(serviceCreds); final String base64OutToken = base64Encode(acceptSecContext(decodedToken, gssContext, loginContext.getSubject())); @@ -100,7 +100,7 @@ public Tuple validateTicket(final byte[] decodedToken, final Pat if (pve.getCause() instanceof GSSException) { throw (GSSException) pve.getCause(); } - throw ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(pve)); + throw ExceptionsHelper.convertToRuntime(pve.getException()); } finally { privilegedLogoutNoThrow(loginContext); privilegedDisposeNoThrow(gssContext); @@ -185,7 +185,7 @@ private static void privilegedDisposeNoThrow(final GSSContext gssContext) { return null; }); } catch (PrivilegedActionException e) { - LOGGER.debug("Could not dispose GSS Context", (Exception) ExceptionsHelper.unwrapCause(e)); + LOGGER.debug("Could not dispose GSS Context", e.getCause()); } } } @@ -204,7 +204,7 @@ private static void privilegedLogoutNoThrow(final LoginContext loginContext) { return null; }); } catch (PrivilegedActionException e) { - LOGGER.debug("Could not close LoginContext", (Exception) ExceptionsHelper.unwrapCause(e)); + LOGGER.debug("Could not close LoginContext", e.getCause()); } } } 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 6dc2b4f2af868..876ebdc574136 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 @@ -27,13 +27,13 @@ public void testKerberosRealmSettings() throws IOException { 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 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); + final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, true); - assertThat(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings), equalTo(keyTabPathConfig)); + 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)); From f70d0b7845304070c58b46e3a136952f41c7cbee Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 22 Jun 2018 17:54:25 +1000 Subject: [PATCH 23/24] [Kerberos] Fix the failing test --- .../authc/kerberos/KerberosAuthenticationTokenTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 31fb7f27d3ffe..eaba796b41fe4 100644 --- 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 @@ -25,7 +25,7 @@ public class KerberosAuthenticationTokenTests extends ESTestCase { - private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; + private static final String UNAUTHENTICATED_PRINCIPAL_NAME = ""; public void testExtractTokenForValidAuthorizationHeader() throws IOException { final String base64Token = Base64.getEncoder().encodeToString(randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); From a324de15801a62e301c40365d5646d5d70db3aeb Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Sat, 23 Jun 2018 17:23:08 +1000 Subject: [PATCH 24/24] [Kerberos] Address review comments --- .../kerberos/support/KerberosTicketValidator.java | 11 +++++++++-- .../support/KerberosTicketValidatorTests.java | 6 +++--- .../kerberos/support/SimpleKdcLdapServerTests.java | 2 +- .../security/authc/kerberos/support/SpnegoClient.java | 4 ++-- 4 files changed, 15 insertions(+), 8 deletions(-) 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 921dce9ef2812..61e8f7500d5f5 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 @@ -89,7 +89,7 @@ public Tuple validateTicket(final byte[] decodedToken, final Pat loginContext = serviceLogin(keytabPath.toString(), krbDebug); GSSCredential serviceCreds = createCredentials(gssManager, loginContext.getSubject()); gssContext = gssManager.createContext(serviceCreds); - final String base64OutToken = base64Encode(acceptSecContext(decodedToken, gssContext, loginContext.getSubject())); + 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); @@ -107,7 +107,14 @@ public Tuple validateTicket(final byte[] decodedToken, final Pat } } - private String base64Encode(final byte[] outToken) { + /** + * 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); } 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 1e5578617f2df..602bb1265fe11 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 @@ -36,7 +36,7 @@ public void testKerbTicketGeneratedForDifferentServerFailsValidation() throws Ex final String clientUserName = randomFrom(clientUserNames); try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName("differentServer"));) { - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); final Environment env = TestEnvironment.newEnvironment(globalSettings); @@ -63,7 +63,7 @@ public void testWhenKeyTabWithInvalidContentFailsValidation() final String clientUserName = randomFrom(clientUserNames); try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames)));) { - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); final Path ktabPath = writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); @@ -81,7 +81,7 @@ public void testValidKebrerosTicket() throws PrivilegedActionException, GSSExcep final String clientUserName = randomFrom(clientUserNames); try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(randomFrom(serviceUserNames)));) { - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); final Environment env = TestEnvironment.newEnvironment(globalSettings); 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 06796a6a33421..c82d4e9502a4f 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 @@ -53,7 +53,7 @@ public void testClientServiceMutualAuthentication() throws PrivilegedActionExcep final String clientUserName = randomFrom(clientUserNames); try (SpnegoClient spnegoClient = new SpnegoClient(principalName(clientUserName), new SecureString("pwd".toCharArray()), principalName(serviceUserName));) { - final String base64KerbToken = spnegoClient.getBase64TicketForSpnegoHeader(); + final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); final KerberosAuthenticationToken kerbAuthnToken = new KerberosAuthenticationToken(Base64.getDecoder().decode(base64KerbToken)); 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 index 4195194c7ed5d..527953f8b2d46 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/support/SpnegoClient.java @@ -94,13 +94,13 @@ class SpnegoClient implements AutoCloseable { } /** - * GSSContext initiator side handling, initiates sec context and returns the + * 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 getBase64TicketForSpnegoHeader() 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);