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
This commit is contained in:
Gary Helmling 2011-03-05 01:10:07 +00:00
parent 2a530e891c
commit 4d28e95a61
6 changed files with 262 additions and 52 deletions

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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.
*
* <p><strong>This is only applicable when
* running on secure Hadoop</strong> -- see
* {@link org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String)}.
* On regular Hadoop (without security features), this will safely be ignored.
* </p>
*
* @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() {
try {
ugi = (UserGroupInformation) callStatic("getCurrentUGI");
} catch (RuntimeException re) {
throw re;
} catch (Exception e) {
throw new UndeclaredThrowableException(e,
"Unexpected exception HadoopUser<init>");
}
}
private HadoopUser(UserGroupInformation ugi) {
@ -143,30 +176,46 @@ public abstract class User {
@Override
public <T> T runAs(PrivilegedAction<T> action) {
UserGroupInformation previous =
(UserGroupInformation) callStatic("getCurrentUGI");
T result = null;
UserGroupInformation previous = null;
try {
previous = (UserGroupInformation) callStatic("getCurrentUGI");
try {
if (ugi != null) {
callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
new Object[]{ugi});
}
T result = action.run();
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()");
}
return result;
}
@Override
public <T> T runAs(PrivilegedExceptionAction<T> action)
throws IOException, InterruptedException {
T result = null;
try {
UserGroupInformation previous =
(UserGroupInformation) callStatic("getCurrentUGI");
try {
if (ugi != null) {
callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
new Object[]{ugi});
}
T result = null;
try {
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() {
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() {
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> T runAs(PrivilegedAction<T> 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> T runAs(PrivilegedExceptionAction<T> action)
throws IOException, InterruptedException {
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) {
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) {
try {
Method m = UserGroupInformation.class.getMethod(methodName, types);
return m.invoke(instance, args);
} catch (NoSuchMethodException nsme) {
LOG.fatal("Can't find method "+methodName+" in UserGroupInformation!",
nsme);
} catch (Exception e) {
LOG.fatal("Error calling method "+methodName, e);
Class[] types, Object[] args) throws Exception {
return call(UserGroupInformation.class, instance, methodName, types, args);
}
return null;
private static <T> Object call(Class<T> clazz, T instance, String methodName,
Class[] types, Object[] args) throws Exception {
try {
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) {
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;
}
}
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();
}
}

View File

@ -459,6 +459,45 @@
used for client / server RPC call marshalling.
</description>
</property>
<!-- The following properties configure authentication information for
HBase processes when using Kerberos security. There are no default
values, included here for documentation purposes -->
<property>
<name>hbase.master.keytab.file</name>
<value></value>
<description>Full path to the kerberos keytab file to use for logging in
the configured HMaster server principal.
</description>
</property>
<property>
<name>hbase.master.kerberos.principal</name>
<value></value>
<description>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.
</description>
</property>
<property>
<name>hbase.regionserver.keytab.file</name>
<value></value>
<description>Full path to the kerberos keytab file to use for logging in
the configured HRegionServer server principal.
</description>
</property>
<property>
<name>hbase.regionserver.kerberos.principal</name>
<value></value>
<description>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
</description>
</property>
<property>
<name>zookeeper.session.timeout</name>
<value>180000</value>

View File

@ -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,8 +43,8 @@ public class TestUser {
public void testRunAs() throws Exception {
Configuration conf = HBaseConfiguration.create();
final User user = User.createUserForTesting(conf, "testuser", new String[]{"foo"});
final PrivilegedAction<String> action = new PrivilegedAction<String>(){
public String run() {
final PrivilegedExceptionAction<String> action = new PrivilegedExceptionAction<String>(){
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",