Author: František Dvořák Date: Tue Feb 2 19:35:01 2016 +0100 HADOOP-12617. SPNEGO authentication request to non-default realm gets default realm name inserted in target server principal. (mattf) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java index ca0fce2..23cbe94 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java @@ -42,7 +42,7 @@ /* Return the Kerberos login module name */ public static String getKrb5LoginModuleName() { - return System.getProperty("java.vendor").contains("IBM") + return (IBM_JAVA) ? "com.ibm.security.auth.module.Krb5LoginModule" : "com.sun.security.auth.module.Krb5LoginModule"; } @@ -72,7 +72,7 @@ public static String getDefaultRealm() Class classRef; Method getInstanceMethod; Method getDefaultRealmMethod; - if (System.getProperty("java.vendor").contains("IBM")) { + if (IBM_JAVA) { classRef = Class.forName("com.ibm.security.krb5.internal.Config"); } else { classRef = Class.forName("sun.security.krb5.Config"); @@ -83,17 +83,79 @@ public static String getDefaultRealm() new Class[0]); return (String)getDefaultRealmMethod.invoke(kerbConf, new Object[0]); } - + + public static String getDefaultRealmProtected() { + String realmString = null; + try { + realmString = getDefaultRealm(); + } catch (RuntimeException rte) { + //silently catch everything + } catch (Exception e) { + //silently return null + } + return realmString; + } + + /* + * For a Service Host Principal specification, map the host's domain + * to kerberos realm, as specified by krb5.conf [domain_realm] mappings. + * Unfortunately the mapping routines are private to the security.krb5 + * package, so have to construct a PrincipalName instance to derive the realm. + * + * Many things can go wrong with Kerberos configuration, and this is not + * the place to be throwing exceptions to help debug them. Nor do we choose + * to make potentially voluminous logs on every call to a communications API. + * So we simply swallow all exceptions from the underlying libraries and + * return null if we can't get a good value for the realmString. + * + * @param shortprinc A service principal name with host fqdn as instance, e.g. + * "HTTP/myhost.mydomain" + * @return String value of Kerberos realm, mapped from host fqdn + * May be default realm, or may be null. + */ + public static String getDomainRealm(String shortprinc) { + Class classRef; + Object principalName; //of type sun.security.krb5.PrincipalName or IBM equiv + String realmString = null; + try { + if (IBM_JAVA) { + classRef = Class.forName("com.ibm.security.krb5.PrincipalName"); + } else { + classRef = Class.forName("sun.security.krb5.PrincipalName"); + } + int tKrbNtSrvHst = classRef.getField("KRB_NT_SRV_HST").getInt(null); + principalName = classRef.getConstructor(String.class, int.class). + newInstance(shortprinc, tKrbNtSrvHst); + realmString = (String)classRef.getMethod("getRealmString", new Class[0]). + invoke(principalName, new Object[0]); + } catch (RuntimeException rte) { + //silently catch everything + } catch (Exception e) { + //silently return default realm (which may itself be null) + } + if (null == realmString || realmString.equals("")) { + return getDefaultRealmProtected(); + } else { + return realmString; + } + } + /* Return fqdn of the current host */ static String getLocalHostName() throws UnknownHostException { return InetAddress.getLocalHost().getCanonicalHostName(); } /** - * Create Kerberos principal for a given service and hostname. It converts + * Create Kerberos principal for a given service and hostname, + * inferring realm from the fqdn of the hostname. It converts * hostname to lower case. If hostname is null or "0.0.0.0", it uses * dynamically looked-up fqdn of the current host instead. - * + * If domain_realm mappings are inadequately specified, it will + * use default_realm, per usual Kerberos behavior. + * If default_realm also gives a null value, then a principal + * without realm will be returned, which by Kerberos definitions is + * just another way to specify default realm. + * * @param service * Service for which you want to generate the principal. * @param hostname @@ -102,15 +164,26 @@ static String getLocalHostName() throws UnknownHostException { * @throws UnknownHostException * If no IP address for the local host could be found. */ - public static final String getServicePrincipal(String service, String hostname) + public static final String getServicePrincipal(String service, + String hostname) throws UnknownHostException { String fqdn = hostname; + String shortprinc = null; + String realmString = null; if (null == fqdn || fqdn.equals("") || fqdn.equals("0.0.0.0")) { fqdn = getLocalHostName(); } // convert hostname to lowercase as kerberos does not work with hostnames // with uppercase characters. - return service + "/" + fqdn.toLowerCase(Locale.US); + fqdn = fqdn.toLowerCase(Locale.US); + shortprinc = service + "/" + fqdn; + // Obtain the realm name inferred from the domain of the host + realmString = getDomainRealm(shortprinc); + if (null == realmString || realmString.equals("")) { + return shortprinc; + } else { + return shortprinc + "@" + realmString; + } } /** diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestKerberosUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestKerberosUtil.java index b0e8f04..064dfaa 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestKerberosUtil.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestKerberosUtil.java @@ -18,9 +18,11 @@ import java.io.File; import java.io.IOException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.regex.Pattern; import org.apache.directory.server.kerberos.shared.keytab.Keytab; @@ -51,31 +53,48 @@ public void deleteKeytab() { } @Test - public void testGetServerPrincipal() throws IOException { + public void testGetServerPrincipal() + throws IOException, UnknownHostException { String service = "TestKerberosUtil"; String localHostname = KerberosUtil.getLocalHostName(); String testHost = "FooBar"; + String defaultRealm = KerberosUtil.getDefaultRealmProtected(); + + String atDefaultRealm; + if (defaultRealm == null || defaultRealm.equals("")) { + atDefaultRealm = ""; + } else { + atDefaultRealm = "@" + defaultRealm; + } + // check that the test environment is as expected + Assert.assertEquals("testGetServerPrincipal assumes localhost realm is default", + KerberosUtil.getDomainRealm(service + "/" + localHostname.toLowerCase(Locale.US)), + defaultRealm); + Assert.assertEquals("testGetServerPrincipal assumes realm of testHost 'FooBar' is default", + KerberosUtil.getDomainRealm(service + "/" + testHost.toLowerCase(Locale.US)), + defaultRealm); // send null hostname Assert.assertEquals("When no hostname is sent", - service + "/" + localHostname.toLowerCase(), + service + "/" + localHostname.toLowerCase(Locale.US) + atDefaultRealm, KerberosUtil.getServicePrincipal(service, null)); // send empty hostname Assert.assertEquals("When empty hostname is sent", - service + "/" + localHostname.toLowerCase(), + service + "/" + localHostname.toLowerCase(Locale.US) + atDefaultRealm, KerberosUtil.getServicePrincipal(service, "")); // send 0.0.0.0 hostname Assert.assertEquals("When 0.0.0.0 hostname is sent", - service + "/" + localHostname.toLowerCase(), + service + "/" + localHostname.toLowerCase(Locale.US) + atDefaultRealm, KerberosUtil.getServicePrincipal(service, "0.0.0.0")); // send uppercase hostname Assert.assertEquals("When uppercase hostname is sent", - service + "/" + testHost.toLowerCase(), + service + "/" + testHost.toLowerCase(Locale.US) + atDefaultRealm, KerberosUtil.getServicePrincipal(service, testHost)); // send lowercase hostname Assert.assertEquals("When lowercase hostname is sent", - service + "/" + testHost.toLowerCase(), - KerberosUtil.getServicePrincipal(service, testHost.toLowerCase())); + service + "/" + testHost.toLowerCase(Locale.US) + atDefaultRealm, + KerberosUtil.getServicePrincipal( + service, testHost.toLowerCase(Locale.US))); } @Test @@ -157,4 +176,4 @@ private void createKeyTab(String fileName, String[] principalNames) keytab.setEntries(lstEntries); keytab.write(new File(testKeytab)); } -} \ No newline at end of file +} diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index 51acf2d..3489431 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -320,6 +320,9 @@ Release 2.6.0 - 2014-11-18 BUG FIXES + HADOOP-12617. SPNEGO authentication request to non-default realm gets + default realm name inserted in target server principal. (mattf) + HADOOP-11182. GraphiteSink emits wrong timestamps (Sascha Coenen via raviprak) HADOOP-10781. Unportable getgrouplist() usage breaks FreeBSD (Dmitry