From a0df473fdca8c024134f6223c66397c0ce6d30d8 Mon Sep 17 00:00:00 2001 From: Reid Chan Date: Wed, 18 Jul 2018 17:15:56 +0800 Subject: [PATCH] HBASE-20886 [Auth] Support keytab login in hbase client --- .../hadoop/hbase/client/AsyncConnectionImpl.java | 16 +++ .../hadoop/hbase/client/ConnectionFactory.java | 31 ++--- .../hbase/client/ConnectionImplementation.java | 26 ++++ .../java/org/apache/hadoop/hbase/AuthUtil.java | 135 +++++++++++++++------ .../java/org/apache/hadoop/hbase/HConstants.java | 4 + .../org/apache/hadoop/hbase/security/User.java | 43 +++++++ .../apache/hadoop/hbase/security/UserProvider.java | 23 +++- .../hbase/client/TestConnectionImplementation.java | 27 +++++ .../hadoop/hbase/security/HBaseKerberosUtils.java | 19 +++ .../TestUsersOperationsWithSecureHadoop.java | 62 ++++++++-- 10 files changed, 323 insertions(+), 63 deletions(-) diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncConnectionImpl.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncConnectionImpl.java index cf26756d91..a05764ee9b 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncConnectionImpl.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncConnectionImpl.java @@ -21,6 +21,9 @@ import static org.apache.hadoop.hbase.client.ConnectionUtils.NO_NONCE_GENERATOR; import static org.apache.hadoop.hbase.client.ConnectionUtils.getStubKey; import static org.apache.hadoop.hbase.client.NonceGenerator.CLIENT_NONCES_ENABLED_KEY; +import org.apache.hadoop.hbase.AuthUtil; +import org.apache.hadoop.hbase.ChoreService; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; import org.apache.hbase.thirdparty.io.netty.util.HashedWheelTimer; @@ -99,10 +102,15 @@ class AsyncConnectionImpl implements AsyncConnection { private final AtomicReference> masterStubMakeFuture = new AtomicReference<>(); + private ChoreService authService; + public AsyncConnectionImpl(Configuration conf, AsyncRegistry registry, String clusterId, User user) { this.conf = conf; this.user = user; + if (user.isLoginFromKeytab()) { + spawnRenewalChore(user.getUGI()); + } this.connConf = new AsyncConnectionConfiguration(conf); this.registry = registry; this.rpcClient = RpcClientFactory.createClient(conf, clusterId); @@ -119,6 +127,11 @@ class AsyncConnectionImpl implements AsyncConnection { } } + private void spawnRenewalChore(final UserGroupInformation user) { + authService = new ChoreService("Relogin service"); + authService.scheduleChore(AuthUtil.getAuthRenewalChore(user)); + } + @Override public Configuration getConfiguration() { return conf; @@ -128,6 +141,9 @@ class AsyncConnectionImpl implements AsyncConnection { public void close() { IOUtils.closeQuietly(rpcClient); IOUtils.closeQuietly(registry); + if (authService != null) { + authService.shutdown(); + } } @Override diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java index 1712a5480a..6a90d63b80 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java @@ -20,10 +20,12 @@ package org.apache.hadoop.hbase.client; import java.io.IOException; import java.lang.reflect.Constructor; +import java.security.PrivilegedExceptionAction; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.AuthUtil; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.yetus.audience.InterfaceAudience; import org.apache.hadoop.hbase.security.User; @@ -84,7 +86,8 @@ public class ConnectionFactory { * @return Connection object for conf */ public static Connection createConnection() throws IOException { - return createConnection(HBaseConfiguration.create(), null, null); + Configuration conf = HBaseConfiguration.create(); + return createConnection(conf, null, AuthUtil.loginClient(conf)); } /** @@ -111,7 +114,7 @@ public class ConnectionFactory { * @return Connection object for conf */ public static Connection createConnection(Configuration conf) throws IOException { - return createConnection(conf, null, null); + return createConnection(conf, null, AuthUtil.loginClient(conf)); } /** @@ -140,7 +143,7 @@ public class ConnectionFactory { */ public static Connection createConnection(Configuration conf, ExecutorService pool) throws IOException { - return createConnection(conf, pool, null); + return createConnection(conf, pool, AuthUtil.loginClient(conf)); } /** @@ -196,13 +199,8 @@ public class ConnectionFactory { * @param pool the thread pool to use for batch operations * @return Connection object for conf */ - public static Connection createConnection(Configuration conf, ExecutorService pool, User user) - throws IOException { - if (user == null) { - UserProvider provider = UserProvider.instantiate(conf); - user = provider.getCurrent(); - } - + public static Connection createConnection(Configuration conf, ExecutorService pool, + final User user) throws IOException { String className = conf.get(ClusterConnection.HBASE_CLIENT_CONNECTION_IMPL, ConnectionImplementation.class.getName()); Class clazz; @@ -216,7 +214,9 @@ public class ConnectionFactory { Constructor constructor = clazz.getDeclaredConstructor(Configuration.class, ExecutorService.class, User.class); constructor.setAccessible(true); - return (Connection) constructor.newInstance(conf, pool, user); + return user.runAs( + (PrivilegedExceptionAction)() -> + (Connection) constructor.newInstance(conf, pool, user)); } catch (Exception e) { throw new IOException(e); } @@ -243,7 +243,7 @@ public class ConnectionFactory { public static CompletableFuture createAsyncConnection(Configuration conf) { User user; try { - user = UserProvider.instantiate(conf).getCurrent(); + user = AuthUtil.loginClient(conf); } catch (IOException e) { CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(e); @@ -269,7 +269,7 @@ public class ConnectionFactory { * @throws IOException */ public static CompletableFuture createAsyncConnection(Configuration conf, - User user) { + final User user) { CompletableFuture future = new CompletableFuture<>(); AsyncRegistry registry = AsyncRegistryFactory.getRegistry(conf); registry.getClusterId().whenComplete((clusterId, error) -> { @@ -284,7 +284,10 @@ public class ConnectionFactory { Class clazz = conf.getClass(HBASE_CLIENT_ASYNC_CONNECTION_IMPL, AsyncConnectionImpl.class, AsyncConnection.class); try { - future.complete(ReflectionUtils.newInstance(clazz, conf, registry, clusterId, user)); + future.complete( + user.runAs((PrivilegedExceptionAction)() -> + ReflectionUtils.newInstance(clazz, conf, registry, clusterId, user)) + ); } catch (Exception e) { future.completeExceptionally(e); } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java index f78005f836..0ca391ab36 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionImplementation.java @@ -46,7 +46,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.AuthUtil; import org.apache.hadoop.hbase.CallQueueTooBigException; +import org.apache.hadoop.hbase.ChoreService; import org.apache.hadoop.hbase.DoNotRetryIOException; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HRegionLocation; @@ -76,6 +78,7 @@ import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.hbase.util.ReflectionUtils; import org.apache.hadoop.hbase.util.Threads; import org.apache.hadoop.ipc.RemoteException; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.yetus.audience.InterfaceAudience; import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; @@ -217,6 +220,8 @@ class ConnectionImplementation implements ClusterConnection, Closeable { /** lock guards against multiple threads trying to query the meta region at the same time */ private final ReentrantLock userRegionLock = new ReentrantLock(); + private ChoreService authService; + /** * constructor * @param conf Configuration object @@ -225,6 +230,9 @@ class ConnectionImplementation implements ClusterConnection, Closeable { ExecutorService pool, User user) throws IOException { this.conf = conf; this.user = user; + if (user.isLoginFromKeytab()) { + spawnRenewalChore(user.getUGI()); + } this.batchPool = pool; this.connectionConfig = new ConnectionConfiguration(conf); this.closed = false; @@ -314,6 +322,11 @@ class ConnectionImplementation implements ClusterConnection, Closeable { } } + private void spawnRenewalChore(final UserGroupInformation user) { + authService = new ChoreService("Relogin service"); + authService.scheduleChore(AuthUtil.getAuthRenewalChore(user)); + } + /** * @param useMetaReplicas */ @@ -1934,6 +1947,9 @@ class ConnectionImplementation implements ClusterConnection, Closeable { if (rpcClient != null) { rpcClient.close(); } + if (authService != null) { + authService.shutdown(); + } } /** @@ -2003,4 +2019,14 @@ class ConnectionImplementation implements ClusterConnection, Closeable { throw new IOException(cause); } } + + @VisibleForTesting + User getConnectionUser() { + return user; + } + + @VisibleForTesting + ChoreService getAuthService() { + return authService; + } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/AuthUtil.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/AuthUtil.java index 5880b8c33b..009e068ae3 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/AuthUtil.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/AuthUtil.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.net.UnknownHostException; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.UserProvider; import org.apache.hadoop.hbase.util.DNS; import org.apache.hadoop.hbase.util.Strings; @@ -78,63 +79,121 @@ public class AuthUtil { } /** - * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket. - * @param conf the hbase service configuration - * @return a ScheduledChore for renewals, if needed, and null otherwise. + * For kerberized cluster, return login user (from kinit or from keytab if specified). + * For non-kerberized cluster, return system user. + * @param conf configuartion file + * @return user + * @throws IOException login exception */ - public static ScheduledChore getAuthChore(Configuration conf) throws IOException { - UserProvider userProvider = UserProvider.instantiate(conf); - // login the principal (if using secure Hadoop) - boolean securityEnabled = - userProvider.isHadoopSecurityEnabled() && userProvider.isHBaseSecurityEnabled(); - if (!securityEnabled) return null; - String host = null; - try { - host = Strings.domainNamePointerToHostName(DNS.getDefaultHost( - conf.get("hbase.client.dns.interface", "default"), - conf.get("hbase.client.dns.nameserver", "default"))); - userProvider.login("hbase.client.keytab.file", "hbase.client.kerberos.principal", host); - } catch (UnknownHostException e) { - LOG.error("Error resolving host name: " + e.getMessage(), e); - throw e; - } catch (IOException e) { - LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e); - throw e; + public static User loginClient(Configuration conf) throws IOException { + UserProvider provider = UserProvider.instantiate(conf); + User user = provider.getCurrent(); + boolean securityOn = provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled(); + if (securityOn) { + if (user.getUGI().hasKerberosCredentials()) { + // There's already user, just return. + return user; + } else if (provider.shouldLoginFromKeytab()) { + // Kerberos is on and client specify a keytab and principal, but client doesn't login yet. + try { + provider.login(HConstants.HBASE_CLIENT_KEYTAB_FILE, + HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL); + } catch (IOException ioe) { + LOG.error("Error while trying to login as user {} through {}, with message: {}.", + HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL, HConstants.HBASE_CLIENT_KEYTAB_FILE, + ioe.getMessage()); + throw ioe; + } + return provider.getCurrent(); + } } + return user; + } - final UserGroupInformation ugi = userProvider.getCurrent().getUGI(); - Stoppable stoppable = new Stoppable() { - private volatile boolean isStopped = false; - - @Override - public void stop(String why) { - isStopped = true; + /** + * For kerberized cluster, return login user (from kinit or from keytab). + * Principal should be the following format: name/fully.qualified.domain.name@REALM. + * For non-kerberized cluster, return system user. + *

+ * NOT recommend to use to method unless you're sure what you're doing, it is for canary only. + * Please use User#loginClient. + * @param conf configuration file + * @return user + * @throws IOException login exception + */ + public static User loginClientAsService(Configuration conf) throws IOException { + UserProvider provider = UserProvider.instantiate(conf); + if (provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled()) { + try { + if (provider.shouldLoginFromKeytab()) { + String host = Strings.domainNamePointerToHostName(DNS.getDefaultHost( + conf.get("hbase.client.dns.interface", "default"), + conf.get("hbase.client.dns.nameserver", "default"))); + provider.login( + HConstants.HBASE_CLIENT_KEYTAB_FILE, HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL, host); + } + } catch (UnknownHostException e) { + LOG.error("Error resolving host name: " + e.getMessage(), e); + throw e; + } catch (IOException e) { + LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e); + throw e; } + } + return provider.getCurrent(); + } - @Override - public boolean isStopped() { - return isStopped; - } - }; + /** + * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket. + * @return a ScheduledChore for renewals. + */ + public static ScheduledChore getAuthRenewalChore(final UserGroupInformation user) { + if (!user.hasKerberosCredentials()) { + return null; + } + Stoppable stoppable = createDummyStoppable(); // if you're in debug mode this is useful to avoid getting spammed by the getTGT() // you can increase this, keeping in mind that the default refresh window is 0.8 // e.g. 5min tgt * 0.8 = 4min refresh so interval is better be way less than 1min final int CHECK_TGT_INTERVAL = 30 * 1000; // 30sec - - ScheduledChore refreshCredentials = - new ScheduledChore("RefreshCredentials", stoppable, CHECK_TGT_INTERVAL) { + return new ScheduledChore("RefreshCredentials", stoppable, CHECK_TGT_INTERVAL) { @Override protected void chore() { try { - ugi.checkTGTAndReloginFromKeytab(); + user.checkTGTAndReloginFromKeytab(); } catch (IOException e) { LOG.error("Got exception while trying to refresh credentials: " + e.getMessage(), e); } } }; + } - return refreshCredentials; + /** + * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket. + * @param conf the hbase service configuration + * @return a ScheduledChore for renewals, if needed, and null otherwise. + */ + public static ScheduledChore getAuthChore(Configuration conf) throws IOException { + User user = loginClientAsService(conf); + return getAuthRenewalChore(user.getUGI()); + } + + @InterfaceAudience.Private + public static Stoppable createDummyStoppable() { + return new Stoppable() { + private volatile boolean isStopped = false; + + @Override + public void stop(String why) { + isStopped = true; + } + + @Override + public boolean isStopped() { + return isStopped; + } + }; } /** diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java index 2e23484cb4..c7441e0b62 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java @@ -1372,6 +1372,10 @@ public final class HConstants { "hbase.util.default.lossycounting.errorrate"; public static final String NOT_IMPLEMENTED = "Not implemented"; + public static final String HBASE_CLIENT_KEYTAB_FILE = "hbase.client.keytab.file"; + + public static final String HBASE_CLIENT_KERBEROS_PRINCIPAL = "hbase.client.keytab.principal"; + private HConstants() { // Can't be instantiated with this ctor. } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/User.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/User.java index af6d442c4f..56c2a6e9df 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/User.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/User.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.util.Methods; import org.apache.hadoop.security.Groups; import org.apache.hadoop.security.SecurityUtil; @@ -136,6 +137,13 @@ public abstract class User { ugi.addToken(token); } + /** + * @return true if user credentials are obtained from keytab. + */ + public boolean isLoginFromKeytab() { + return ugi.isFromKeytab(); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -231,6 +239,16 @@ public abstract class User { SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost); } + /** + * Login with the given keytab and principal. + * @param keytabLocation path of keytab + * @param pricipalName login principal + * @throws IOException underlying exception from UserGroupInformation.loginUserFromKeytab + */ + public static void login(String keytabLocation, String pricipalName) throws IOException { + SecureHadoopUser.login(keytabLocation, pricipalName); + } + /** * Returns whether or not Kerberos authentication is configured for Hadoop. * For non-secure Hadoop, this always returns false. @@ -250,6 +268,18 @@ public abstract class User { return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY)); } + /** + * In secure environment, if a user specified his keytab and principal, + * a hbase client will try to login with them. Otherwise, hbase client will try to obtain + * ticket(through kinit) from system. + * @param conf configuration file + * @return true if keytab and principal are configured + */ + public static boolean shouldLoginFromKeytab(Configuration conf) { + return !conf.get(HConstants.HBASE_CLIENT_KEYTAB_FILE).isEmpty() && + !conf.get(HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL).isEmpty(); + } + /* Concrete implementations */ /** @@ -345,6 +375,19 @@ public abstract class User { } } + /** + * Login through configured keytab and pricipal. + * @param keytabLocation location of keytab + * @param principalName principal in keytab + * @throws IOException exception from UserGroupInformation.loginUserFromKeytab + */ + public static void login(String keytabLocation, String principalName) + throws IOException { + if (isSecurityEnabled()) { + UserGroupInformation.loginUserFromKeytab(principalName, keytabLocation); + } + } + /** * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}. */ diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/UserProvider.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/UserProvider.java index 0f7d4284c8..17796ee56d 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/UserProvider.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/UserProvider.java @@ -160,6 +160,15 @@ public class UserProvider extends BaseConfigurable { return User.isSecurityEnabled(); } + /** + * In secure environment, if a user specified his keytab and principal, + * a hbase client will try to login with them. Otherwise, hbase client will try to obtain + * ticket(through kinit) from system. + */ + public boolean shouldLoginFromKeytab() { + return User.shouldLoginFromKeytab(this.getConf()); + } + /** * @return the current user within the current execution context * @throws IOException if the user cannot be loaded @@ -182,7 +191,8 @@ public class UserProvider extends BaseConfigurable { /** * Log in the current process using the given configuration keys for the credential file and login - * principal. + * principal. It is for SPN(Service Principal Name) login. SPN should be this format, + * servicename/fully.qualified.domain.name@REALM. *

* This is only applicable when running on secure Hadoop -- see * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String). On regular @@ -197,4 +207,15 @@ public class UserProvider extends BaseConfigurable { throws IOException { User.login(getConf(), fileConfKey, principalConfKey, localhost); } + + /** + * Login with given keytab and principal. This can be used for both SPN(Service Principal Name) + * and UPN(User Principal Name) which format should be clientname@REALM. + * @param fileConfKey config name for client keytab + * @param principalConfKey config name for client principal + * @throws IOException underlying exception from UserGroupInformation.loginUserFromKeytab + */ + public void login(String fileConfKey, String principalConfKey) throws IOException { + User.login(getConf().get(fileConfKey), getConf().get(principalConfKey)); + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestConnectionImplementation.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestConnectionImplementation.java index 4d9f39bebe..be8016da0d 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestConnectionImplementation.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestConnectionImplementation.java @@ -43,6 +43,7 @@ import java.util.stream.IntStream; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.ChoreService; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HConstants; @@ -62,12 +63,16 @@ import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.regionserver.HRegionServer; import org.apache.hadoop.hbase.regionserver.Region; import org.apache.hadoop.hbase.regionserver.RegionServerStoppedException; +import org.apache.hadoop.hbase.security.HBaseKerberosUtils; +import org.apache.hadoop.hbase.security.TestUsersOperationsWithSecureHadoop; +import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.testclassification.LargeTests; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.JVMClusterUtil; import org.apache.hadoop.hbase.util.ManualEnvironmentEdge; import org.apache.hadoop.hbase.util.Threads; +import org.apache.hadoop.security.UserGroupInformation; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -1080,4 +1085,26 @@ public class TestConnectionImplementation { TEST_UTIL.deleteTable(tableName); } } + + @Test + public void testConnectionUserLogin() throws Exception { + TestUsersOperationsWithSecureHadoop.setUp(); + try { + String clientKeytab = HBaseKerberosUtils.getClientKeytabForTesting(); + String clientPrincipal = HBaseKerberosUtils.getClientPrincipalForTesting(); + Configuration conf = HBaseKerberosUtils.getSecuredConfiguration(); + conf.set(HConstants.HBASE_CLIENT_KEYTAB_FILE, clientKeytab); + conf.set(HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL, clientPrincipal); + UserGroupInformation.setConfiguration(conf); + + Connection connection = ConnectionFactory.createConnection(conf); + User loginUser = ((ConnectionImplementation) connection).getConnectionUser(); + ChoreService authService = ((ConnectionImplementation) connection).getAuthService(); + assertTrue(loginUser.isLoginFromKeytab()); + assertEquals(clientPrincipal, loginUser.getName()); + assertNotNull(authService); + } finally { + TestUsersOperationsWithSecureHadoop.tearDown(); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/HBaseKerberosUtils.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/HBaseKerberosUtils.java index b946e7458a..07922128ed 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/HBaseKerberosUtils.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/HBaseKerberosUtils.java @@ -20,6 +20,7 @@ package org.apache.hadoop.hbase.security; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.HConstants; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,8 @@ public class HBaseKerberosUtils { public static final String KRB_PRINCIPAL = "hbase.regionserver.kerberos.principal"; public static final String MASTER_KRB_PRINCIPAL = "hbase.master.kerberos.principal"; public static final String KRB_KEYTAB_FILE = "hbase.regionserver.keytab.file"; + public static final String CLIENT_PRINCIPAL = HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL; + public static final String CLIENT_KEYTAB = HConstants.HBASE_CLIENT_KEYTAB_FILE; public static boolean isKerberosPropertySetted() { String krbPrincipal = System.getProperty(KRB_PRINCIPAL); @@ -54,6 +57,14 @@ public class HBaseKerberosUtils { setSystemProperty(KRB_KEYTAB_FILE, keytabFile); } + public static void setClientPrincipalForTesting(String clientPrincipal) { + setSystemProperty(CLIENT_PRINCIPAL, clientPrincipal); + } + + public static void setClientKeytabForTesting(String clientKeytab) { + setSystemProperty(CLIENT_KEYTAB, clientKeytab); + } + public static void setSystemProperty(String propertyName, String propertyValue) { System.setProperty(propertyName, propertyValue); } @@ -66,6 +77,14 @@ public class HBaseKerberosUtils { return System.getProperty(KRB_PRINCIPAL); } + public static String getClientPrincipalForTesting() { + return System.getProperty(CLIENT_PRINCIPAL); + } + + public static String getClientKeytabForTesting() { + return System.getProperty(CLIENT_KEYTAB); + } + public static Configuration getConfigurationWoPrincipal() { Configuration conf = HBaseConfiguration.create(); conf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/TestUsersOperationsWithSecureHadoop.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/TestUsersOperationsWithSecureHadoop.java index b69c5d9d6b..91688243ef 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/TestUsersOperationsWithSecureHadoop.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/TestUsersOperationsWithSecureHadoop.java @@ -17,19 +17,24 @@ */ package org.apache.hadoop.hbase.security; -import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getConfigurationWoPrincipal; +import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getClientKeytabForTesting; +import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getClientPrincipalForTesting; import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getKeytabFileForTesting; import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getPrincipalForTesting; import static org.apache.hadoop.hbase.security.HBaseKerberosUtils.getSecuredConfiguration; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; + import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.AuthUtil; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.testclassification.SecurityTests; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.apache.hadoop.minikdc.MiniKdc; @@ -57,12 +62,18 @@ public class TestUsersOperationsWithSecureHadoop { private static String PRINCIPAL; + private static String CLIENT_NAME; + @BeforeClass public static void setUp() throws Exception { KDC = TEST_UTIL.setupMiniKdc(KEYTAB_FILE); PRINCIPAL = "hbase/" + HOST; - KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL); + CLIENT_NAME = "foo"; + KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, CLIENT_NAME); HBaseKerberosUtils.setPrincipalForTesting(PRINCIPAL + "@" + KDC.getRealm()); + HBaseKerberosUtils.setKeytabFileForTesting(KEYTAB_FILE.getAbsolutePath()); + HBaseKerberosUtils.setClientPrincipalForTesting(CLIENT_NAME + "@" + KDC.getRealm()); + HBaseKerberosUtils.setClientKeytabForTesting(KEYTAB_FILE.getAbsolutePath()); } @AfterClass @@ -84,13 +95,8 @@ public class TestUsersOperationsWithSecureHadoop { */ @Test public void testUserLoginInSecureHadoop() throws Exception { - UserGroupInformation defaultLogin = UserGroupInformation.getLoginUser(); - Configuration conf = getConfigurationWoPrincipal(); - User.login(conf, HBaseKerberosUtils.KRB_KEYTAB_FILE, HBaseKerberosUtils.KRB_PRINCIPAL, - "localhost"); - - UserGroupInformation failLogin = UserGroupInformation.getLoginUser(); - assertTrue("ugi should be the same in case fail login", defaultLogin.equals(failLogin)); + // Default login is system user. + UserGroupInformation defaultLogin = UserGroupInformation.getCurrentUser(); String nnKeyTab = getKeytabFileForTesting(); String dnPrincipal = getPrincipalForTesting(); @@ -98,7 +104,7 @@ public class TestUsersOperationsWithSecureHadoop { assertNotNull("KerberosKeytab was not specified", nnKeyTab); assertNotNull("KerberosPrincipal was not specified", dnPrincipal); - conf = getSecuredConfiguration(); + Configuration conf = getSecuredConfiguration(); UserGroupInformation.setConfiguration(conf); User.login(conf, HBaseKerberosUtils.KRB_KEYTAB_FILE, HBaseKerberosUtils.KRB_PRINCIPAL, @@ -107,4 +113,40 @@ public class TestUsersOperationsWithSecureHadoop { assertFalse("ugi should be different in in case success login", defaultLogin.equals(successLogin)); } + + @Test + public void testLoginWithUserKeytabAndPrincipal() throws Exception { + String clientKeytab = getClientKeytabForTesting(); + String clientPrincipal = getClientPrincipalForTesting(); + assertNotNull("Path for client keytab is not specified.", clientKeytab); + assertNotNull("Client principal is not specified.", clientPrincipal); + + Configuration conf = getSecuredConfiguration(); + conf.set(HConstants.HBASE_CLIENT_KEYTAB_FILE, clientKeytab); + conf.set(HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL, clientPrincipal); + UserGroupInformation.setConfiguration(conf); + + UserProvider provider = UserProvider.instantiate(conf); + assertTrue("Client principal or keytab is empty", provider.shouldLoginFromKeytab()); + + provider.login(HConstants.HBASE_CLIENT_KEYTAB_FILE, HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL); + User loginUser = provider.getCurrent(); + assertEquals(CLIENT_NAME, loginUser.getShortName()); + assertEquals(getClientPrincipalForTesting(), loginUser.getName()); + } + + @Test + public void testAuthUtilLogin() throws Exception { + String clientKeytab = getClientKeytabForTesting(); + String clientPrincipal = getClientPrincipalForTesting(); + Configuration conf = getSecuredConfiguration(); + conf.set(HConstants.HBASE_CLIENT_KEYTAB_FILE, clientKeytab); + conf.set(HConstants.HBASE_CLIENT_KERBEROS_PRINCIPAL, clientPrincipal); + UserGroupInformation.setConfiguration(conf); + + User user = AuthUtil.loginClient(conf); + assertTrue(user.isLoginFromKeytab()); + assertEquals(CLIENT_NAME, user.getShortName()); + assertEquals(getClientPrincipalForTesting(), user.getName()); + } } -- 2.15.0