From 022a7372d02d59ec254b6671becbdef30d4f6d3d Mon Sep 17 00:00:00 2001 From: Michael Stack Date: Tue, 9 Nov 2010 16:56:22 +0000 Subject: [PATCH] HBASE-3194 HBase should run on both secure and vanilla versions of Hadoop 0.20 -- forgot to add classes git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1033089 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/hadoop/hbase/security/User.java | 279 ++++++++++++++++++ .../hadoop/hbase/security/TestUser.java | 81 +++++ 2 files changed, 360 insertions(+) create mode 100644 src/main/java/org/apache/hadoop/hbase/security/User.java create mode 100644 src/test/java/org/apache/hadoop/hbase/security/TestUser.java diff --git a/src/main/java/org/apache/hadoop/hbase/security/User.java b/src/main/java/org/apache/hadoop/hbase/security/User.java new file mode 100644 index 00000000000..4ecf12a4ca9 --- /dev/null +++ b/src/main/java/org/apache/hadoop/hbase/security/User.java @@ -0,0 +1,279 @@ +/* + * Copyright 2010 The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.hbase.security; + +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import org.apache.commons.logging.Log; + +/** + * Wrapper to abstract out usage of user and group information in HBase. + * + *

+ * This class provides a common interface for interacting with user and group + * information across changing APIs in different versions of Hadoop. It only + * provides access to the common set of functionality in + * {@link org.apache.hadoop.security.UserGroupInformation} currently needed by + * HBase, but can be extended as needs change. + *

+ * + *

+ * Note: this class does not attempt to support any of the Kerberos + * authentication methods exposed in security-enabled Hadoop (for the moment + * at least), as they're not yet needed. Properly supporting + * authentication is left up to implementation in secure HBase. + *

+ */ +public abstract class User { + private static boolean IS_SECURE_HADOOP = true; + static { + try { + UserGroupInformation.class.getMethod("isSecurityEnabled"); + } catch (NoSuchMethodException nsme) { + IS_SECURE_HADOOP = false; + } + } + private static Log LOG = LogFactory.getLog(User.class); + protected UserGroupInformation ugi; + + /** + * Returns the full user name. For Kerberos principals this will include + * the host and realm portions of the principal name. + * @return + */ + public String getName() { + return ugi.getUserName(); + } + + /** + * Returns the shortened version of the user name -- the portion that maps + * to an operating system user name. + * @return + */ + public abstract String getShortName(); + + /** + * Executes the given action within the context of this user. + */ + public abstract T runAs(PrivilegedAction action); + + /** + * Executes the given action within the context of this user. + */ + public abstract T runAs(PrivilegedExceptionAction action) + throws IOException, InterruptedException; + + public String toString() { + return ugi.toString(); + } + + /** + * Returns the {@code User} instance within current execution context. + */ + public static User getCurrent() { + if (IS_SECURE_HADOOP) { + return new SecureHadoopUser(); + } else { + return new HadoopUser(); + } + } + + /** + * Generates a new {@code User} instance specifically for use in test code. + * @param name the full username + * @param groups the group names to which the test user will belong + * @return a new User instance + */ + public static User createUserForTesting(Configuration conf, + String name, String[] groups) { + if (IS_SECURE_HADOOP) { + return SecureHadoopUser.createUserForTesting(conf, name, groups); + } + return HadoopUser.createUserForTesting(conf, name, groups); + } + + /* Concrete implementations */ + + /** + * Bridges {@link User} calls to invocations of the appropriate methods + * in {@link org.apache.hadoop.security.UserGroupInformation} in regular + * Hadoop 0.20 (ASF Hadoop and other versions without the backported security + * features). + */ + private static class HadoopUser extends User { + + private HadoopUser() { + ugi = (UserGroupInformation) callStatic("getCurrentUGI"); + } + + private HadoopUser(UserGroupInformation ugi) { + this.ugi = ugi; + } + + @Override + public String getShortName() { + return ugi.getUserName(); + } + + @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 = 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(); + } catch (Exception e) { + if (e instanceof IOException) { + throw (IOException)e; + } else if (e instanceof InterruptedException) { + throw (InterruptedException)e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } else { + throw new UndeclaredThrowableException(e, "Unknown exception in runAs()"); + } + } finally { + callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class}, + new Object[]{previous}); + } + return result; + } + + public static User createUserForTesting(Configuration conf, + String name, String[] groups) { + try { + Class c = Class.forName("org.apache.hadoop.security.UnixUserGroupInformation"); + Constructor constructor = c.getConstructor(String.class, String[].class); + if (constructor == null) { + throw new NullPointerException( + ); + } + UserGroupInformation newUser = + (UserGroupInformation)constructor.newInstance(name, groups); + // set user in configuration -- hack for regular hadoop + conf.set("hadoop.job.ugi", newUser.toString()); + return new HadoopUser(newUser); + } catch (ClassNotFoundException cnfe) { + LOG.error("UnixUserGroupInformation not found, is this secure Hadoop?", cnfe); + } catch (NoSuchMethodException nsme) { + LOG.error("No valid constructor found for UnixUserGroupInformation!", nsme); + } catch (Exception e) { + LOG.error("Error instantiating new UnixUserGroupInformation", e); + } + + return null; + } + } + + /** + * Bridges {@code User} invocations to underlying calls to + * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop + * 0.20 and versions 0.21 and above. + */ + private static class SecureHadoopUser extends User { + private SecureHadoopUser() { + ugi = (UserGroupInformation) callStatic("getCurrentUser"); + } + + private SecureHadoopUser(UserGroupInformation ugi) { + this.ugi = ugi; + } + + @Override + public String getShortName() { + return (String)call(ugi, "getShortUserName", null, null); + } + + @Override + public T runAs(PrivilegedAction action) { + return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class}, + new Object[]{action}); + } + + @Override + public T runAs(PrivilegedExceptionAction action) + throws IOException, InterruptedException { + return (T) call(ugi, "doAs", + new Class[]{PrivilegedExceptionAction.class}, + new Object[]{action}); + } + + 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}) + ); + } + } + + /* Reflection helper methods */ + private static Object callStatic(String methodName) { + return call(null, methodName, null, null); + } + + private static Object callStatic(String methodName, Class[] types, + Object[] args) { + 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); + } + return null; + } +} diff --git a/src/test/java/org/apache/hadoop/hbase/security/TestUser.java b/src/test/java/org/apache/hadoop/hbase/security/TestUser.java new file mode 100644 index 00000000000..e5f4cf97e26 --- /dev/null +++ b/src/test/java/org/apache/hadoop/hbase/security/TestUser.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010 The Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security; + +import static org.junit.Assert.*; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.junit.Test; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; + +public class TestUser { + @Test + public void testBasicAttributes() throws Exception { + Configuration conf = HBaseConfiguration.create(); + User user = User.createUserForTesting(conf, "simple", new String[]{"foo"}); + assertEquals("Username should match", "simple", user.getName()); + assertEquals("Short username should match", "simple", user.getShortName()); + // don't test shortening of kerberos names because regular Hadoop doesn't support them + } + + @Test + 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(); + } + }; + + String username = user.runAs(action); + assertEquals("Current user within runAs() should match", + "testuser", username); + + // ensure the next run is correctly set + User user2 = User.createUserForTesting(conf, "testuser2", new String[]{"foo"}); + String username2 = user2.runAs(action); + assertEquals("Second username should match second user", + "testuser2", username2); + + // check the exception version + username = user.runAs(new PrivilegedExceptionAction(){ + public String run() throws Exception { + return User.getCurrent().getName(); + } + }); + assertEquals("User name in runAs() should match", "testuser", username); + + // verify that nested contexts work + user2.runAs(new PrivilegedAction(){ + public Object run() { + String nestedName = user.runAs(action); + assertEquals("Nest name should match nested user", "testuser", nestedName); + assertEquals("Current name should match current user", + "testuser2", User.getCurrent().getName()); + return null; + } + }); + } +}