From 4d28e95a618fb24881708a279f90074b6b9787d9 Mon Sep 17 00:00:00 2001 From: Gary Helmling Date: Sat, 5 Mar 2011 01:10:07 +0000 Subject: [PATCH] HBASE-3582 Allow HMaster and HRegionServer to login from keytab when running on secure Hadoop git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1078225 13f79535-47bb-0310-9956-ffa450edef68 --- CHANGES.txt | 2 + .../apache/hadoop/hbase/master/HMaster.java | 5 + .../hbase/regionserver/HRegionServer.java | 5 + .../apache/hadoop/hbase/security/User.java | 250 ++++++++++++++---- src/main/resources/hbase-default.xml | 39 +++ .../hadoop/hbase/security/TestUser.java | 13 +- 6 files changed, 262 insertions(+), 52 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 80f405410bc..ef1ccce0c1f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -136,6 +136,8 @@ Release 0.90.2 - Unreleased log4j.properties HBASE-3591 completebulkload doesn't honor generic -D options HBASE-3594 Rest server fails because of missing asm jar + HBASE-3582 Allow HMaster and HRegionServer to login from keytab + when on secure Hadoop IMPROVEMENTS HBASE-3542 MultiGet methods in Thrift diff --git a/src/main/java/org/apache/hadoop/hbase/master/HMaster.java b/src/main/java/org/apache/hadoop/hbase/master/HMaster.java index 41c8c8f5782..991a426b240 100644 --- a/src/main/java/org/apache/hadoop/hbase/master/HMaster.java +++ b/src/main/java/org/apache/hadoop/hbase/master/HMaster.java @@ -75,6 +75,7 @@ import org.apache.hadoop.hbase.master.handler.TableModifyFamilyHandler; import org.apache.hadoop.hbase.master.metrics.MasterMetrics; import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.replication.regionserver.Replication; +import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.InfoServer; import org.apache.hadoop.hbase.util.Pair; @@ -203,6 +204,10 @@ implements HMasterInterface, HMasterRegionInterface, MasterServices, Server { 0); // this is a DNC w/o high priority handlers this.address = new HServerAddress(rpcServer.getListenerAddress()); + // initialize server principal (if using secure Hadoop) + User.login(conf, "hbase.master.keytab.file", + "hbase.master.kerberos.principal", this.address.getHostname()); + // set the thread name now we have an address setName(MASTER + "-" + this.address); diff --git a/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index edd14baceb6..a1218298540 100644 --- a/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -118,6 +118,7 @@ import org.apache.hadoop.hbase.regionserver.metrics.RegionServerMetrics; import org.apache.hadoop.hbase.regionserver.wal.HLog; import org.apache.hadoop.hbase.regionserver.wal.WALObserver; import org.apache.hadoop.hbase.replication.regionserver.Replication; +import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.CompressionTest; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; @@ -344,6 +345,10 @@ public class HRegionServer implements HRegionInterface, HBaseRPCErrorHandler, throw new NullPointerException("Server address cannot be null; " + "hbase-958 debugging"); } + + // login the server principal (if using secure Hadoop) + User.login(conf, "hbase.regionserver.keytab.file", + "hbase.regionserver.kerberos.principal", serverInfo.getHostname()); } private static final int NORMAL_QOS = 0; diff --git a/src/main/java/org/apache/hadoop/hbase/security/User.java b/src/main/java/org/apache/hadoop/hbase/security/User.java index 4b5e9e80cda..90dc28c405c 100644 --- a/src/main/java/org/apache/hadoop/hbase/security/User.java +++ b/src/main/java/org/apache/hadoop/hbase/security/User.java @@ -26,6 +26,7 @@ import org.apache.hadoop.security.UserGroupInformation; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.security.PrivilegedAction; @@ -96,7 +97,7 @@ public abstract class User { /** * Returns the {@code User} instance within current execution context. */ - public static User getCurrent() { + public static User getCurrent() throws IOException { if (IS_SECURE_HADOOP) { return new SecureHadoopUser(); } else { @@ -118,6 +119,31 @@ public abstract class User { return HadoopUser.createUserForTesting(conf, name, groups); } + /** + * Log in the current process using the given configuration keys for the + * credential file and login principal. + * + *

This is only applicable when + * running on secure Hadoop -- see + * {@link org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String)}. + * On regular Hadoop (without security features), this will safely be ignored. + *

+ * + * @param conf The configuration data to use + * @param fileConfKey Property key used to configure path to the credential file + * @param principalConfKey Property key used to configure login principal + * @param localhost Current hostname to use in any credentials + * @throws IOException underlying exception from SecurityUtil.login() call + */ + public static void login(Configuration conf, String fileConfKey, + String principalConfKey, String localhost) throws IOException { + if (IS_SECURE_HADOOP) { + SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost); + } else { + HadoopUser.login(conf, fileConfKey, principalConfKey, localhost); + } + } + /* Concrete implementations */ /** @@ -129,7 +155,14 @@ public abstract class User { private static class HadoopUser extends User { private HadoopUser() { - ugi = (UserGroupInformation) callStatic("getCurrentUGI"); + try { + ugi = (UserGroupInformation) callStatic("getCurrentUGI"); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected exception HadoopUser"); + } } private HadoopUser(UserGroupInformation ugi) { @@ -143,30 +176,46 @@ public abstract class User { @Override public T runAs(PrivilegedAction action) { - UserGroupInformation previous = - (UserGroupInformation) callStatic("getCurrentUGI"); - if (ugi != null) { - callStatic("setCurrentUser", new Class[]{UserGroupInformation.class}, - new Object[]{ugi}); + T result = null; + UserGroupInformation previous = null; + try { + previous = (UserGroupInformation) callStatic("getCurrentUGI"); + try { + if (ugi != null) { + callStatic("setCurrentUser", new Class[]{UserGroupInformation.class}, + new Object[]{ugi}); + } + result = action.run(); + } finally { + callStatic("setCurrentUser", new Class[]{UserGroupInformation.class}, + new Object[]{previous}); + } + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected exception in runAs()"); } - T result = action.run(); - callStatic("setCurrentUser", new Class[]{UserGroupInformation.class}, - new Object[]{previous}); return result; } @Override public T runAs(PrivilegedExceptionAction action) throws IOException, InterruptedException { - UserGroupInformation previous = - (UserGroupInformation) callStatic("getCurrentUGI"); - if (ugi != null) { - callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class}, - new Object[]{ugi}); - } T result = null; try { - result = action.run(); + UserGroupInformation previous = + (UserGroupInformation) callStatic("getCurrentUGI"); + try { + if (ugi != null) { + callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class}, + new Object[]{ugi}); + } + result = action.run(); + } finally { + callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class}, + new Object[]{previous}); + } } catch (Exception e) { if (e instanceof IOException) { throw (IOException)e; @@ -177,9 +226,6 @@ public abstract class User { } else { throw new UndeclaredThrowableException(e, "Unknown exception in runAs()"); } - } finally { - callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class}, - new Object[]{previous}); } return result; } @@ -199,14 +245,22 @@ public abstract class User { conf.set("hadoop.job.ugi", newUser.toString()); return new HadoopUser(newUser); } catch (ClassNotFoundException cnfe) { - LOG.error("UnixUserGroupInformation not found, is this secure Hadoop?", cnfe); + throw new RuntimeException( + "UnixUserGroupInformation not found, is this secure Hadoop?", cnfe); } catch (NoSuchMethodException nsme) { - LOG.error("No valid constructor found for UnixUserGroupInformation!", nsme); + throw new RuntimeException( + "No valid constructor found for UnixUserGroupInformation!", nsme); + } catch (RuntimeException re) { + throw re; } catch (Exception e) { - LOG.error("Error instantiating new UnixUserGroupInformation", e); + throw new UndeclaredThrowableException(e, + "Unexpected exception instantiating new UnixUserGroupInformation"); } + } - return null; + public static void login(Configuration conf, String fileConfKey, + String principalConfKey, String localhost) throws IOException { + LOG.info("Skipping login, not running on secure Hadoop"); } } @@ -216,8 +270,17 @@ public abstract class User { * 0.20 and versions 0.21 and above. */ private static class SecureHadoopUser extends User { - private SecureHadoopUser() { - ugi = (UserGroupInformation) callStatic("getCurrentUser"); + private SecureHadoopUser() throws IOException { + try { + ugi = (UserGroupInformation) callStatic("getCurrentUser"); + } catch (IOException ioe) { + throw ioe; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected exception getting current secure user"); + } } private SecureHadoopUser(UserGroupInformation ugi) { @@ -226,54 +289,149 @@ public abstract class User { @Override public String getShortName() { - return (String)call(ugi, "getShortUserName", null, null); + try { + return (String)call(ugi, "getShortUserName", null, null); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected error getting user short name"); + } } @Override public T runAs(PrivilegedAction action) { - return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class}, - new Object[]{action}); + try { + return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class}, + new Object[]{action}); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected exception in runAs()"); + } } @Override public T runAs(PrivilegedExceptionAction action) throws IOException, InterruptedException { - return (T) call(ugi, "doAs", - new Class[]{PrivilegedExceptionAction.class}, - new Object[]{action}); + try { + return (T) call(ugi, "doAs", + new Class[]{PrivilegedExceptionAction.class}, + new Object[]{action}); + } catch (IOException ioe) { + throw ioe; + } catch (InterruptedException ie) { + throw ie; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unexpected exception in runAs(PrivilegedExceptionAction)"); + } } public static User createUserForTesting(Configuration conf, String name, String[] groups) { - return new SecureHadoopUser( - (UserGroupInformation)callStatic("createUserForTesting", - new Class[]{String.class, String[].class}, - new Object[]{name, groups}) - ); + try { + return new SecureHadoopUser( + (UserGroupInformation)callStatic("createUserForTesting", + new Class[]{String.class, String[].class}, + new Object[]{name, groups}) + ); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Error creating secure test user"); + } + } + + public static void login(Configuration conf, String fileConfKey, + String principalConfKey, String localhost) throws IOException { + // check for SecurityUtil class + try { + Class c = Class.forName("org.apache.hadoop.security.SecurityUtil"); + Class[] types = new Class[]{ + Configuration.class, String.class, String.class, String.class }; + Object[] args = new Object[]{ + conf, fileConfKey, principalConfKey, localhost }; + call(c, null, "login", types, args); + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Unable to login using " + + "org.apache.hadoop.security.Security.login(). SecurityUtil class " + + "was not found! Is this a version of secure Hadoop?", cnfe); + } catch (IOException ioe) { + throw ioe; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new UndeclaredThrowableException(e, + "Unhandled exception in User.login()"); + } } } /* Reflection helper methods */ - private static Object callStatic(String methodName) { + private static Object callStatic(String methodName) throws Exception { return call(null, methodName, null, null); } private static Object callStatic(String methodName, Class[] types, - Object[] args) { + Object[] args) throws Exception { return call(null, methodName, types, args); } private static Object call(UserGroupInformation instance, String methodName, - Class[] types, Object[] args) { + Class[] types, Object[] args) throws Exception { + return call(UserGroupInformation.class, instance, methodName, types, args); + } + + private static Object call(Class clazz, T instance, String methodName, + Class[] types, Object[] args) throws Exception { try { - Method m = UserGroupInformation.class.getMethod(methodName, types); + Method m = clazz.getMethod(methodName, types); return m.invoke(instance, args); + } catch (IllegalArgumentException arge) { + LOG.fatal("Constructed invalid call. class="+clazz.getName()+ + " method=" + methodName + " types=" + stringify(types), arge); + throw arge; } catch (NoSuchMethodException nsme) { - LOG.fatal("Can't find method "+methodName+" in UserGroupInformation!", - nsme); - } catch (Exception e) { - LOG.fatal("Error calling method "+methodName, e); + throw new IllegalArgumentException( + "Can't find method "+methodName+" in "+clazz.getName()+"!", nsme); + } catch (InvocationTargetException ite) { + // unwrap the underlying exception and rethrow + if (ite.getTargetException() != null) { + if (ite.getTargetException() instanceof Exception) { + throw (Exception)ite.getTargetException(); + } else if (ite.getTargetException() instanceof Error) { + throw (Error)ite.getTargetException(); + } + } + throw new UndeclaredThrowableException(ite, + "Unknown exception invoking "+clazz.getName()+"."+methodName+"()"); + } catch (IllegalAccessException iae) { + throw new IllegalArgumentException( + "Denied access calling "+clazz.getName()+"."+methodName+"()", iae); + } catch (SecurityException se) { + LOG.fatal("SecurityException calling method. class="+clazz.getName()+ + " method=" + methodName + " types=" + stringify(types), se); + throw se; } - return null; + } + + private static String stringify(Class[] classes) { + StringBuilder buf = new StringBuilder(); + if (classes != null) { + for (Class c : classes) { + if (buf.length() > 0) { + buf.append(","); + } + buf.append(c.getName()); + } + } else { + buf.append("NULL"); + } + return buf.toString(); } } diff --git a/src/main/resources/hbase-default.xml b/src/main/resources/hbase-default.xml index 9f188a46337..f4b5ebbcab6 100644 --- a/src/main/resources/hbase-default.xml +++ b/src/main/resources/hbase-default.xml @@ -459,6 +459,45 @@ used for client / server RPC call marshalling. + + + + hbase.master.keytab.file + + Full path to the kerberos keytab file to use for logging in + the configured HMaster server principal. + + + + hbase.master.kerberos.principal + + Ex. "hbase/_HOST@EXAMPLE.COM". The kerberos principal name + that should be used to run the HMaster process. The principal name should + be in the form: user/hostname@DOMAIN. If "_HOST" is used as the hostname + portion, it will be replaced with the actual hostname of the running + instance. + + + + hbase.regionserver.keytab.file + + Full path to the kerberos keytab file to use for logging in + the configured HRegionServer server principal. + + + + hbase.regionserver.kerberos.principal + + Ex. "hbase/_HOST@EXAMPLE.COM". The kerberos principal name + that should be used to run the HRegionServer process. The principal name + should be in the form: user/hostname@DOMAIN. If "_HOST" is used as the + hostname portion, it will be replaced with the actual hostname of the + running instance. An entry for this principal must exist in the file + specified in hbase.regionserver.keytab.file + + zookeeper.session.timeout 180000 diff --git a/src/test/java/org/apache/hadoop/hbase/security/TestUser.java b/src/test/java/org/apache/hadoop/hbase/security/TestUser.java index e5f4cf97e26..6e497daf90e 100644 --- a/src/test/java/org/apache/hadoop/hbase/security/TestUser.java +++ b/src/test/java/org/apache/hadoop/hbase/security/TestUser.java @@ -25,6 +25,7 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.junit.Test; +import java.io.IOException; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; @@ -42,10 +43,10 @@ public class TestUser { public void testRunAs() throws Exception { Configuration conf = HBaseConfiguration.create(); final User user = User.createUserForTesting(conf, "testuser", new String[]{"foo"}); - final PrivilegedAction action = new PrivilegedAction(){ - public String run() { - User u = User.getCurrent(); - return u.getName(); + final PrivilegedExceptionAction action = new PrivilegedExceptionAction(){ + public String run() throws IOException { + User u = User.getCurrent(); + return u.getName(); } }; @@ -68,8 +69,8 @@ public class TestUser { assertEquals("User name in runAs() should match", "testuser", username); // verify that nested contexts work - user2.runAs(new PrivilegedAction(){ - public Object run() { + user2.runAs(new PrivilegedExceptionAction(){ + public Object run() throws IOException, InterruptedException{ String nestedName = user.runAs(action); assertEquals("Nest name should match nested user", "testuser", nestedName); assertEquals("Current name should match current user",