From 47f03bc9fe9c8c94a567d29765cef2266f2993d0 Mon Sep 17 00:00:00 2001 From: Kihwal Lee Date: Thu, 8 May 2014 18:26:58 +0000 Subject: [PATCH] HADOOP-10158. SPNEGO should work with multiple interfaces/SPNs. Contributed by Daryn Sharp. git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1593362 13f79535-47bb-0310-9956-ffa450edef68 --- .../server/KerberosAuthenticationHandler.java | 92 +++++++++++-------- .../TestKerberosAuthenticationHandler.java | 71 +++++++++++++- .../hadoop-common/CHANGES.txt | 3 + .../hadoop-hdfs/src/site/apt/WebHDFS.apt.vm | 2 +- 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/KerberosAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/KerberosAuthenticationHandler.java index 327fc5e541..333eb8e43a 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/KerberosAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/KerberosAuthenticationHandler.java @@ -34,16 +34,18 @@ import javax.security.auth.login.LoginException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import java.io.File; import java.io.IOException; -import java.security.Principal; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.regex.Pattern; import static org.apache.hadoop.util.PlatformName.IBM_JAVA; @@ -140,10 +142,10 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { */ public static final String NAME_RULES = TYPE + ".name.rules"; - private String principal; private String keytab; private GSSManager gssManager; - private LoginContext loginContext; + private Subject serverSubject = new Subject(); + private List loginContexts = new ArrayList(); /** * Initializes the authentication handler instance. @@ -159,7 +161,7 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { @Override public void init(Properties config) throws ServletException { try { - principal = config.getProperty(PRINCIPAL, principal); + String principal = config.getProperty(PRINCIPAL); if (principal == null || principal.trim().length() == 0) { throw new ServletException("Principal not defined in configuration"); } @@ -170,23 +172,40 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { if (!new File(keytab).exists()) { throw new ServletException("Keytab does not exist: " + keytab); } + + // use all SPNEGO principals in the keytab if a principal isn't + // specifically configured + final String[] spnegoPrincipals; + if (principal.equals("*")) { + spnegoPrincipals = KerberosUtil.getPrincipalNames( + keytab, Pattern.compile("HTTP/.*")); + if (spnegoPrincipals.length == 0) { + throw new ServletException("Principals do not exist in the keytab"); + } + } else { + spnegoPrincipals = new String[]{principal}; + } String nameRules = config.getProperty(NAME_RULES, null); if (nameRules != null) { KerberosName.setRules(nameRules); } - Set principals = new HashSet(); - principals.add(new KerberosPrincipal(principal)); - Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); - - KerberosConfiguration kerberosConfiguration = new KerberosConfiguration(keytab, principal); - - LOG.info("Login using keytab "+keytab+", for principal "+principal); - loginContext = new LoginContext("", subject, null, kerberosConfiguration); - loginContext.login(); - - Subject serverSubject = loginContext.getSubject(); + for (String spnegoPrincipal : spnegoPrincipals) { + LOG.info("Login using keytab {}, for principal {}", + keytab, principal); + final KerberosConfiguration kerberosConfiguration = + new KerberosConfiguration(keytab, spnegoPrincipal); + final LoginContext loginContext = + new LoginContext("", serverSubject, null, kerberosConfiguration); + try { + loginContext.login(); + } catch (LoginException le) { + LOG.warn("Failed to login as [{}]", spnegoPrincipal, le); + throw new AuthenticationException(le); + } + loginContexts.add(loginContext); + } try { gssManager = Subject.doAs(serverSubject, new PrivilegedExceptionAction() { @@ -198,7 +217,6 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { } catch (PrivilegedActionException ex) { throw ex.getException(); } - LOG.info("Initialized, principal [{}] from keytab [{}]", principal, keytab); } catch (Exception ex) { throw new ServletException(ex); } @@ -211,14 +229,16 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { */ @Override public void destroy() { - try { - if (loginContext != null) { + keytab = null; + serverSubject = null; + for (LoginContext loginContext : loginContexts) { + try { loginContext.logout(); - loginContext = null; + } catch (LoginException ex) { + LOG.warn(ex.getMessage(), ex); } - } catch (LoginException ex) { - LOG.warn(ex.getMessage(), ex); } + loginContexts.clear(); } /** @@ -233,12 +253,12 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { } /** - * Returns the Kerberos principal used by the authentication handler. + * Returns the Kerberos principals used by the authentication handler. * - * @return the Kerberos principal used by the authentication handler. + * @return the Kerberos principals used by the authentication handler. */ - protected String getPrincipal() { - return principal; + protected Set getPrincipals() { + return serverSubject.getPrincipals(KerberosPrincipal.class); } /** @@ -304,7 +324,7 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { authorization = authorization.substring(KerberosAuthenticator.NEGOTIATE.length()).trim(); final Base64 base64 = new Base64(0); final byte[] clientToken = base64.decode(authorization); - Subject serverSubject = loginContext.getSubject(); + final String serverName = request.getServerName(); try { token = Subject.doAs(serverSubject, new PrivilegedExceptionAction() { @@ -314,15 +334,15 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler { GSSContext gssContext = null; GSSCredential gssCreds = null; try { - if (IBM_JAVA) { - // IBM JDK needs non-null credentials to be passed to createContext here, with - // SPNEGO mechanism specified, otherwise JGSS will use its default mechanism - // only, which is Kerberos V5. - gssCreds = gssManager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, - new Oid[]{KerberosUtil.getOidInstance("GSS_SPNEGO_MECH_OID"), - KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID")}, - GSSCredential.ACCEPT_ONLY); - } + gssCreds = gssManager.createCredential( + gssManager.createName( + KerberosUtil.getServicePrincipal("HTTP", serverName), + KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL")), + GSSCredential.INDEFINITE_LIFETIME, + new Oid[]{ + KerberosUtil.getOidInstance("GSS_SPNEGO_MECH_OID"), + KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID")}, + GSSCredential.ACCEPT_ONLY); gssContext = gssManager.createContext(gssCreds); byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length); if (serverToken != null && serverToken.length > 0) { diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java index ab793b7c61..408563f299 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestKerberosAuthenticationHandler.java @@ -18,6 +18,7 @@ import org.apache.hadoop.security.authentication.KerberosTestUtils; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; import org.apache.hadoop.security.authentication.util.KerberosName; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.ietf.jgss.GSSContext; @@ -30,10 +31,18 @@ import org.junit.Test; import org.mockito.Mockito; import org.ietf.jgss.Oid; +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import java.io.File; +import java.security.Principal; +import java.util.Arrays; +import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.concurrent.Callable; public class TestKerberosAuthenticationHandler @@ -110,8 +119,65 @@ public class TestKerberosAuthenticationHandler @Test(timeout=60000) public void testInit() throws Exception { - Assert.assertEquals(KerberosTestUtils.getServerPrincipal(), handler.getPrincipal()); Assert.assertEquals(KerberosTestUtils.getKeytabFile(), handler.getKeytab()); + Set principals = handler.getPrincipals(); + Principal expectedPrincipal = + new KerberosPrincipal(KerberosTestUtils.getServerPrincipal()); + Assert.assertTrue(principals.contains(expectedPrincipal)); + Assert.assertEquals(1, principals.size()); + } + + // dynamic configuration of HTTP principals + @Test(timeout=60000) + public void testDynamicPrincipalDiscovery() throws Exception { + String[] keytabUsers = new String[]{ + "HTTP/host1", "HTTP/host2", "HTTP2/host1", "XHTTP/host" + }; + String keytab = KerberosTestUtils.getKeytabFile(); + getKdc().createPrincipal(new File(keytab), keytabUsers); + + // destroy handler created in setUp() + handler.destroy(); + Properties props = new Properties(); + props.setProperty(KerberosAuthenticationHandler.KEYTAB, keytab); + props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, "*"); + handler = getNewAuthenticationHandler(); + handler.init(props); + + Assert.assertEquals(KerberosTestUtils.getKeytabFile(), handler.getKeytab()); + + Set loginPrincipals = handler.getPrincipals(); + for (String user : keytabUsers) { + Principal principal = new KerberosPrincipal( + user + "@" + KerberosTestUtils.getRealm()); + boolean expected = user.startsWith("HTTP/"); + Assert.assertEquals("checking for "+user, expected, + loginPrincipals.contains(principal)); + } + } + + // dynamic configuration of HTTP principals + @Test(timeout=60000) + public void testDynamicPrincipalDiscoveryMissingPrincipals() throws Exception { + String[] keytabUsers = new String[]{"hdfs/localhost"}; + String keytab = KerberosTestUtils.getKeytabFile(); + getKdc().createPrincipal(new File(keytab), keytabUsers); + + // destroy handler created in setUp() + handler.destroy(); + Properties props = new Properties(); + props.setProperty(KerberosAuthenticationHandler.KEYTAB, keytab); + props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, "*"); + handler = getNewAuthenticationHandler(); + try { + handler.init(props); + Assert.fail("init should have failed"); + } catch (ServletException ex) { + Assert.assertEquals("Principals do not exist in the keytab", + ex.getCause().getMessage()); + } catch (Throwable t) { + Assert.fail("wrong exception: "+t); + } } @Test(timeout=60000) @@ -190,7 +256,8 @@ public class TestKerberosAuthenticationHandler Mockito.when(request.getHeader(KerberosAuthenticator.AUTHORIZATION)) .thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token); - + Mockito.when(request.getServerName()).thenReturn("localhost"); + AuthenticationToken authToken = handler.authenticate(request, response); if (authToken != null) { diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index 3012bc90ce..a19bafa260 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -377,6 +377,9 @@ Release 2.5.0 - UNRELEASED HADOOP-10467. Enable proxyuser specification to support list of users in addition to list of groups (Benoy Antony via Arpit Agarwal) + HADOOP-10158. SPNEGO should work with multiple interfaces/SPNs. + (daryn via kihwal) + OPTIMIZATIONS BUG FIXES diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/site/apt/WebHDFS.apt.vm b/hadoop-hdfs-project/hadoop-hdfs/src/site/apt/WebHDFS.apt.vm index 46f59cbdf6..48bcca3320 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/site/apt/WebHDFS.apt.vm +++ b/hadoop-hdfs-project/hadoop-hdfs/src/site/apt/WebHDFS.apt.vm @@ -146,7 +146,7 @@ WebHDFS REST API *-------------------------------------------------+---------------------------------------------------+ | <<>> | Enable/disable WebHDFS in Namenodes and Datanodes | *-------------------------------------------------+---------------------------------------------------+ -| <<>> | The HTTP Kerberos principal used by Hadoop-Auth in the HTTP endpoint. The HTTP Kerberos principal MUST start with 'HTTP/' per Kerberos HTTP SPNEGO specification. | +| <<>> | The HTTP Kerberos principal used by Hadoop-Auth in the HTTP endpoint. The HTTP Kerberos principal MUST start with 'HTTP/' per Kerberos HTTP SPNEGO specification. A value of "*" will use all HTTP principals found in the keytab. | *-------------------------------------------------+---------------------------------------------------+ | <<>> | The Kerberos keytab file with the credentials for the HTTP Kerberos principal used by Hadoop-Auth in the HTTP endpoint. | *-------------------------------------------------+---------------------------------------------------+