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",