diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeys.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeys.java index ba6e4e2d45d..043e52a52e2 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeys.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeys.java @@ -355,17 +355,6 @@ public class CommonConfigurationKeys extends CommonConfigurationKeysPublic { public static final String HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS = "hadoop.user.group.metrics.percentiles.intervals"; - /* When creating UGI with UserGroupInformation(Subject), treat the passed - * subject external if set to true, and assume the owner of the subject - * should do the credential renewal. - * - * This is a temporary config to solve the compatibility issue with - * HADOOP-13558 and HADOOP-13805 fix, see the jiras for discussions. - */ - public static final String HADOOP_TREAT_SUBJECT_EXTERNAL_KEY = - "hadoop.treat.subject.external"; - public static final boolean HADOOP_TREAT_SUBJECT_EXTERNAL_DEFAULT = false; - public static final String RPC_METRICS_QUANTILE_ENABLE = "rpc.metrics.quantile.enable"; public static final boolean RPC_METRICS_QUANTILE_ENABLE_DEFAULT = false; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java index 726e81111d1..003a51c14ff 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java @@ -18,8 +18,6 @@ package org.apache.hadoop.security; import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS; -import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_TREAT_SUBJECT_EXTERNAL_KEY; -import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_TREAT_SUBJECT_EXTERNAL_DEFAULT; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN_DEFAULT; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_TOKEN_FILES; @@ -42,12 +40,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.security.auth.DestroyFailedException; import javax.security.auth.Subject; @@ -56,6 +56,7 @@ import javax.security.auth.kerberos.KerberosTicket; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; +import javax.security.auth.login.Configuration.Parameters; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; @@ -190,10 +191,8 @@ public boolean commit() throws LoginException { } return true; } - Principal user = null; - // if we are using kerberos, try it out - if (isAuthenticationMethodEnabled(AuthenticationMethod.KERBEROS)) { - user = getCanonicalUser(KerberosPrincipal.class); + Principal user = getCanonicalUser(KerberosPrincipal.class); + if (user != null) { if (LOG.isDebugEnabled()) { LOG.debug("using kerberos user:"+user); } @@ -222,7 +221,11 @@ public boolean commit() throws LoginException { User userEntry = null; try { - userEntry = new User(user.getName()); + // LoginContext will be attached later unless it's an external + // subject. + AuthenticationMethod authMethod = (user instanceof KerberosPrincipal) + ? AuthenticationMethod.KERBEROS : AuthenticationMethod.SIMPLE; + userEntry = new User(user.getName(), authMethod, null); } catch (Exception e) { throw (LoginException)(new LoginException(e.toString()).initCause(e)); } @@ -277,28 +280,6 @@ public static void reattachMetrics() { private static long kerberosMinSecondsBeforeRelogin; /** The configuration to use */ - /* - * This config is a temporary one for backward compatibility. - * It means whether to treat the subject passed to - * UserGroupInformation(Subject) as external. If true, - * - no renewal thread will be created to do the renew credential - * - reloginFromKeytab() and reloginFromTicketCache will not renew - * credential. - * and it assumes that the owner of the subject to renew; if false, it means - * to retain the old behavior prior to fixing HADOOP-13558 and HADOOP-13805. - * The default is false. - */ - private static boolean treatSubjectExternal = false; - - /* - * Some test need the renewal thread to be created even if it does - * UserGroupInformation.loginUserFromSubject(subject); - * The test code may set this variable to true via - * setEnableRenewThreadCreationForTest(boolean) - * method. - */ - private static boolean enableRenewThreadCreationForTest = false; - private static Configuration conf; @@ -364,15 +345,6 @@ private static synchronized void initialize(Configuration conf, metrics.getGroupsQuantiles = getGroupsQuantiles; } } - - treatSubjectExternal = conf.getBoolean(HADOOP_TREAT_SUBJECT_EXTERNAL_KEY, - HADOOP_TREAT_SUBJECT_EXTERNAL_DEFAULT); - if (treatSubjectExternal) { - LOG.info("Config " + HADOOP_TREAT_SUBJECT_EXTERNAL_KEY + " is set to " - + "true, the owner of the subject passed to " - + " UserGroupInformation(Subject) is supposed to renew the " - + "credential."); - } } /** @@ -387,18 +359,6 @@ public static void setConfiguration(Configuration conf) { initialize(conf, true); } - @InterfaceAudience.Private - @VisibleForTesting - static void setEnableRenewThreadCreationForTest(boolean b) { - enableRenewThreadCreationForTest = b; - } - - @InterfaceAudience.Private - @VisibleForTesting - static boolean getEnableRenewThreadCreationForTest() { - return enableRenewThreadCreationForTest; - } - @InterfaceAudience.Private @VisibleForTesting public static void reset() { @@ -408,7 +368,6 @@ public static void reset() { kerberosMinSecondsBeforeRelogin = 0; setLoginUser(null); HadoopKerberosName.setRules(null); - setEnableRenewThreadCreationForTest(false); } /** @@ -431,17 +390,13 @@ private static boolean isAuthenticationMethodEnabled(AuthenticationMethod method /** * Information about the logged in user. */ - private static UserGroupInformation loginUser = null; - private static String keytabPrincipal = null; - private static String keytabFile = null; + private static final AtomicReference loginUserRef = + new AtomicReference<>(); private final Subject subject; // All non-static fields must be read-only caches that come from the subject. private final User user; - private final boolean isKeytab; - private final boolean isKrbTkt; - private final boolean isLoginExternal; - + private static String OS_LOGIN_MODULE_NAME; private static Class OS_PRINCIPAL_CLASS; @@ -540,134 +495,10 @@ public String toString() { return realUser.toString(); } } - - /** - * A JAAS configuration that defines the login modules that we want - * to use for login. - */ - private static class HadoopConfiguration - extends javax.security.auth.login.Configuration { - private static final String SIMPLE_CONFIG_NAME = "hadoop-simple"; - private static final String USER_KERBEROS_CONFIG_NAME = - "hadoop-user-kerberos"; - private static final String KEYTAB_KERBEROS_CONFIG_NAME = - "hadoop-keytab-kerberos"; - private static final Map BASIC_JAAS_OPTIONS = - new HashMap(); - static { - String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG"); - if (jaasEnvVar != null && "true".equalsIgnoreCase(jaasEnvVar)) { - BASIC_JAAS_OPTIONS.put("debug", "true"); - } - } - - private static final AppConfigurationEntry OS_SPECIFIC_LOGIN = - new AppConfigurationEntry(OS_LOGIN_MODULE_NAME, - LoginModuleControlFlag.REQUIRED, - BASIC_JAAS_OPTIONS); - private static final AppConfigurationEntry HADOOP_LOGIN = - new AppConfigurationEntry(HadoopLoginModule.class.getName(), - LoginModuleControlFlag.REQUIRED, - BASIC_JAAS_OPTIONS); - private static final Map USER_KERBEROS_OPTIONS = - new HashMap(); - static { - if (IBM_JAVA) { - USER_KERBEROS_OPTIONS.put("useDefaultCcache", "true"); - } else { - USER_KERBEROS_OPTIONS.put("doNotPrompt", "true"); - USER_KERBEROS_OPTIONS.put("useTicketCache", "true"); - } - String ticketCache = System.getenv("KRB5CCNAME"); - if (ticketCache != null) { - if (IBM_JAVA) { - // The first value searched when "useDefaultCcache" is used. - System.setProperty("KRB5CCNAME", ticketCache); - } else { - USER_KERBEROS_OPTIONS.put("ticketCache", ticketCache); - } - } - USER_KERBEROS_OPTIONS.put("renewTGT", "true"); - USER_KERBEROS_OPTIONS.putAll(BASIC_JAAS_OPTIONS); - } - private static final AppConfigurationEntry USER_KERBEROS_LOGIN = - new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(), - LoginModuleControlFlag.OPTIONAL, - USER_KERBEROS_OPTIONS); - private static final Map KEYTAB_KERBEROS_OPTIONS = - new HashMap(); - static { - if (IBM_JAVA) { - KEYTAB_KERBEROS_OPTIONS.put("credsType", "both"); - } else { - KEYTAB_KERBEROS_OPTIONS.put("doNotPrompt", "true"); - KEYTAB_KERBEROS_OPTIONS.put("useKeyTab", "true"); - KEYTAB_KERBEROS_OPTIONS.put("storeKey", "true"); - } - KEYTAB_KERBEROS_OPTIONS.put("refreshKrb5Config", "true"); - KEYTAB_KERBEROS_OPTIONS.putAll(BASIC_JAAS_OPTIONS); - } - private static final AppConfigurationEntry KEYTAB_KERBEROS_LOGIN = - new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(), - LoginModuleControlFlag.REQUIRED, - KEYTAB_KERBEROS_OPTIONS); - - private static final AppConfigurationEntry[] SIMPLE_CONF = - new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, HADOOP_LOGIN}; - - private static final AppConfigurationEntry[] USER_KERBEROS_CONF = - new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, USER_KERBEROS_LOGIN, - HADOOP_LOGIN}; - - private static final AppConfigurationEntry[] KEYTAB_KERBEROS_CONF = - new AppConfigurationEntry[]{KEYTAB_KERBEROS_LOGIN, HADOOP_LOGIN}; - - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String appName) { - if (SIMPLE_CONFIG_NAME.equals(appName)) { - return SIMPLE_CONF; - } else if (USER_KERBEROS_CONFIG_NAME.equals(appName)) { - return USER_KERBEROS_CONF; - } else if (KEYTAB_KERBEROS_CONFIG_NAME.equals(appName)) { - if (IBM_JAVA) { - KEYTAB_KERBEROS_OPTIONS.put("useKeytab", - prependFileAuthority(keytabFile)); - } else { - KEYTAB_KERBEROS_OPTIONS.put("keyTab", keytabFile); - } - KEYTAB_KERBEROS_OPTIONS.put("principal", keytabPrincipal); - return KEYTAB_KERBEROS_CONF; - } - return null; - } - } - - private static String prependFileAuthority(String keytabPath) { - return keytabPath.startsWith("file://") ? keytabPath - : "file://" + keytabPath; - } - - /** - * Represents a javax.security configuration that is created at runtime. - */ - private static class DynamicConfiguration - extends javax.security.auth.login.Configuration { - private AppConfigurationEntry[] ace; - - DynamicConfiguration(AppConfigurationEntry[] ace) { - this.ace = ace; - } - - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String appName) { - return ace; - } - } - - private static LoginContext + private static HadoopLoginContext newLoginContext(String appName, Subject subject, - javax.security.auth.login.Configuration loginConf) + HadoopConfiguration loginConf) throws LoginException { // Temporarily switch the thread's ContextClassLoader to match this // class's classloader, so that we can properly load HadoopLoginModule @@ -676,16 +507,20 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String appName) { ClassLoader oldCCL = t.getContextClassLoader(); t.setContextClassLoader(HadoopLoginModule.class.getClassLoader()); try { - return new LoginContext(appName, subject, null, loginConf); + return new HadoopLoginContext(appName, subject, loginConf); } finally { t.setContextClassLoader(oldCCL); } } - private LoginContext getLogin() { - return user.getLogin(); + // return the LoginContext only if it's managed by the ugi. externally + // managed login contexts will be ignored. + private HadoopLoginContext getLogin() { + LoginContext login = user.getLogin(); + return (login instanceof HadoopLoginContext) + ? (HadoopLoginContext)login : null; } - + private void setLogin(LoginContext login) { user.setLogin(login); } @@ -698,31 +533,22 @@ private void setLogin(LoginContext login) { * @param subject the user's subject */ UserGroupInformation(Subject subject) { - this(subject, treatSubjectExternal); - } - - /** - * Create a UGI from the given subject. - * @param subject the subject - * @param isLoginExternal if the subject's keytab is managed by other UGI. - * Setting this to true will prevent UGI from attempting - * to login the keytab, or to renew it. - */ - private UserGroupInformation(Subject subject, final boolean isLoginExternal) { this.subject = subject; + // do not access ANY private credentials since they are mutable + // during a relogin. no principal locking necessary since + // relogin/logout does not remove User principal. this.user = subject.getPrincipals(User.class).iterator().next(); - - this.isKeytab = KerberosUtil.hasKerberosKeyTab(subject); - this.isKrbTkt = KerberosUtil.hasKerberosTicket(subject); - this.isLoginExternal = isLoginExternal; + if (user == null || user.getName() == null) { + throw new IllegalStateException("Subject does not contain a valid User"); + } } - + /** * checks if logged in using kerberos * @return true if the subject logged via keytab or has a Kerberos TGT */ public boolean hasKerberosCredentials() { - return isKeytab || isKrbTkt; + return user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS; } /** @@ -732,8 +558,7 @@ public boolean hasKerberosCredentials() { */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized - static UserGroupInformation getCurrentUser() throws IOException { + public static UserGroupInformation getCurrentUser() throws IOException { AccessControlContext context = AccessController.getContext(); Subject subject = Subject.getSubject(context); if (subject == null || subject.getPrincipals(User.class).isEmpty()) { @@ -779,53 +604,10 @@ public static UserGroupInformation getUGIFromTicketCache( if (!isAuthenticationMethodEnabled(AuthenticationMethod.KERBEROS)) { return getBestUGI(null, user); } - try { - Map krbOptions = new HashMap(); - if (IBM_JAVA) { - krbOptions.put("useDefaultCcache", "true"); - // The first value searched when "useDefaultCcache" is used. - System.setProperty("KRB5CCNAME", ticketCache); - } else { - krbOptions.put("doNotPrompt", "true"); - krbOptions.put("useTicketCache", "true"); - krbOptions.put("useKeyTab", "false"); - krbOptions.put("ticketCache", ticketCache); - } - krbOptions.put("renewTGT", "false"); - krbOptions.putAll(HadoopConfiguration.BASIC_JAAS_OPTIONS); - AppConfigurationEntry ace = new AppConfigurationEntry( - KerberosUtil.getKrb5LoginModuleName(), - LoginModuleControlFlag.REQUIRED, - krbOptions); - DynamicConfiguration dynConf = - new DynamicConfiguration(new AppConfigurationEntry[]{ ace }); - LoginContext login = newLoginContext( - HadoopConfiguration.USER_KERBEROS_CONFIG_NAME, null, dynConf); - login.login(); - - Subject loginSubject = login.getSubject(); - Set loginPrincipals = loginSubject.getPrincipals(); - if (loginPrincipals.isEmpty()) { - throw new RuntimeException("No login principals found!"); - } - if (loginPrincipals.size() != 1) { - LOG.warn("found more than one principal in the ticket cache file " + - ticketCache); - } - User ugiUser = new User(loginPrincipals.iterator().next().getName(), - AuthenticationMethod.KERBEROS, login); - loginSubject.getPrincipals().add(ugiUser); - UserGroupInformation ugi = new UserGroupInformation(loginSubject, false); - ugi.setLogin(login); - ugi.setAuthenticationMethod(AuthenticationMethod.KERBEROS); - return ugi; - } catch (LoginException le) { - KerberosAuthException kae = - new KerberosAuthException(FAILURE_TO_LOGIN, le); - kae.setUser(user); - kae.setTicketCacheFile(ticketCache); - throw kae; - } + LoginParams params = new LoginParams(); + params.put(LoginParam.PRINCIPAL, user); + params.put(LoginParam.CCACHE, ticketCache); + return doSubjectLogin(null, params); } /** @@ -848,29 +630,40 @@ public static UserGroupInformation getUGIFromSubject(Subject subject) throw new KerberosAuthException(SUBJECT_MUST_CONTAIN_PRINCIPAL); } - KerberosPrincipal principal = - subject.getPrincipals(KerberosPrincipal.class).iterator().next(); - - User ugiUser = new User(principal.getName(), - AuthenticationMethod.KERBEROS, null); - subject.getPrincipals().add(ugiUser); - UserGroupInformation ugi = new UserGroupInformation(subject); - ugi.setLogin(null); - ugi.setAuthenticationMethod(AuthenticationMethod.KERBEROS); - return ugi; + // null params indicate external subject login. no login context will + // be attached. + return doSubjectLogin(subject, null); } /** - * Get the currently logged in user. + * Get the currently logged in user. If no explicit login has occurred, + * the user will automatically be logged in with either kerberos credentials + * if available, or as the local OS user, based on security settings. * @return the logged in user * @throws IOException if login fails */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized - static UserGroupInformation getLoginUser() throws IOException { + public static UserGroupInformation getLoginUser() throws IOException { + UserGroupInformation loginUser = loginUserRef.get(); + // a potential race condition exists only for the initial creation of + // the login user. there's no need to penalize all subsequent calls + // with sychronization overhead so optimistically create a login user + // and discard if we lose the race. if (loginUser == null) { - loginUserFromSubject(null); + UserGroupInformation newLoginUser = createLoginUser(null); + do { + // it's extremely unlikely that the login user will be non-null + // (lost CAS race), but be nulled before the subsequent get, but loop + // for correctness. + if (loginUserRef.compareAndSet(null, newLoginUser)) { + loginUser = newLoginUser; + // only spawn renewal if this login user is the winner. + loginUser.spawnAutoRenewalThreadForUserCreds(false); + } else { + loginUser = loginUserRef.get(); + } + } while (loginUser == null); } return loginUser; } @@ -902,30 +695,15 @@ public static String trimLoginMethod(String userName) { */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized - static void loginUserFromSubject(Subject subject) throws IOException { - ensureInitialized(); - boolean externalSubject = false; - try { - if (subject == null) { - subject = new Subject(); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Treat subject external: " + treatSubjectExternal - + ". When true, assuming keytab is managed extenally since " - + " logged in from subject"); - } - externalSubject = treatSubjectExternal; - } - LoginContext login = - newLoginContext(authenticationMethod.getLoginAppName(), - subject, new HadoopConfiguration()); - login.login(); + public static void loginUserFromSubject(Subject subject) throws IOException { + setLoginUser(createLoginUser(subject)); + } - UserGroupInformation realUser = - new UserGroupInformation(subject, externalSubject); - realUser.setLogin(login); - realUser.setAuthenticationMethod(authenticationMethod); + private static + UserGroupInformation createLoginUser(Subject subject) throws IOException { + UserGroupInformation realUser = doSubjectLogin(subject, null); + UserGroupInformation loginUser = null; + try { // If the HADOOP_PROXY_USER environment variable or property // is specified, create a proxy user as the logged in user. String proxyUser = System.getenv(HADOOP_PROXY_USER); @@ -974,38 +752,64 @@ static void loginUserFromSubject(Subject subject) throws IOException { LOG.debug("Loaded {} tokens", cred.numberOfTokens()); loginUser.addCredentials(cred); } - loginUser.spawnAutoRenewalThreadForUserCreds(); - } catch (LoginException le) { - LOG.debug("failure to login", le); - throw new KerberosAuthException(FAILURE_TO_LOGIN, le); + } catch (IOException ioe) { + LOG.debug("failure to load login credentials", ioe); + throw ioe; } if (LOG.isDebugEnabled()) { LOG.debug("UGI loginUser:"+loginUser); - } + } + return loginUser; } @InterfaceAudience.Private @InterfaceStability.Unstable @VisibleForTesting - public synchronized static void setLoginUser(UserGroupInformation ugi) { + public static void setLoginUser(UserGroupInformation ugi) { // if this is to become stable, should probably logout the currently // logged in ugi if it's different - loginUser = ugi; + loginUserRef.set(ugi); } + private String getKeytab() { + HadoopLoginContext login = getLogin(); + return (login != null) + ? login.getConfiguration().getParameters().get(LoginParam.KEYTAB) + : null; + } + /** - * Is this user logged in from a keytab file? + * Is the ugi managed by the UGI or an external subject? + * @return true if managed by UGI. + */ + private boolean isHadoopLogin() { + // checks if the private hadoop login context is managing the ugi. + return getLogin() != null; + } + + /** + * Is this user logged in from a keytab file managed by the UGI? * @return true if the credentials are from a keytab file. */ public boolean isFromKeytab() { - return isKeytab; + // can't simply check if keytab is present since a relogin failure will + // have removed the keytab from priv creds. instead, check login params. + return hasKerberosCredentials() && isHadoopLogin() && getKeytab() != null; } + /** + * Is this user logged in from a ticket (but no keytab) managed by the UGI? + * @return true if the credentials are from a ticket cache. + */ + private boolean isFromTicket() { + return hasKerberosCredentials() && isHadoopLogin() && getKeytab() == null; + } + /** * Get the Kerberos TGT * @return the user's TGT or null if none was found */ - private synchronized KerberosTicket getTGT() { + private KerberosTicket getTGT() { Set tickets = subject .getPrivateCredentials(KerberosTicket.class); for (KerberosTicket ticket : tickets) { @@ -1022,23 +826,20 @@ private long getRefreshTime(KerberosTicket tgt) { return start + (long) ((end - start) * TICKET_RENEW_WINDOW); } - /** - * Should relogin if security is enabled using Kerberos, and - * the Subject is not owned by another UGI. - * @return true if this UGI should relogin - */ private boolean shouldRelogin() { - return isSecurityEnabled() - && user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS - && !isLoginExternal; + return hasKerberosCredentials() && isHadoopLogin(); } - /**Spawn a thread to do periodic renewals of kerberos credentials*/ - private void spawnAutoRenewalThreadForUserCreds() { - if (getEnableRenewThreadCreationForTest()) { - LOG.warn("Spawning thread to auto renew user credential since " + - " enableRenewThreadCreationForTest was set to true."); - } else if (!shouldRelogin() || isKeytab) { + @InterfaceAudience.Private + @InterfaceStability.Unstable + @VisibleForTesting + /** + * Spawn a thread to do periodic renewals of kerberos credentials from + * a ticket cache. NEVER directly call this method. + * @param force - used by tests to forcibly spawn thread + */ + void spawnAutoRenewalThreadForUserCreds(boolean force) { + if (!force && (!shouldRelogin() || isFromKeytab())) { return; } @@ -1149,38 +950,16 @@ static long getNextTgtRenewalTime(final long tgtEndTime, final long now, */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized + public static void loginUserFromKeytab(String user, String path ) throws IOException { if (!isSecurityEnabled()) return; - keytabFile = path; - keytabPrincipal = user; - Subject subject = new Subject(); - LoginContext login; - long start = 0; - try { - login = newLoginContext(HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, - subject, new HadoopConfiguration()); - start = Time.now(); - login.login(); - metrics.loginSuccess.add(Time.now() - start); - loginUser = new UserGroupInformation(subject, false); - loginUser.setLogin(login); - loginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS); - } catch (LoginException le) { - if (start > 0) { - metrics.loginFailure.add(Time.now() - start); - } - KerberosAuthException kae = new KerberosAuthException(LOGIN_FAILURE, le); - kae.setUser(user); - kae.setKeytabFile(path); - throw kae; - } - LOG.info("Login successful for user " + keytabPrincipal - + " using keytab file " + keytabFile); + setLoginUser(loginUserFromKeytabAndReturnUGI(user, path)); + LOG.info("Login successful for user " + user + + " using keytab file " + path); } /** @@ -1195,11 +974,11 @@ static void loginUserFromKeytab(String user, @InterfaceAudience.Public @InterfaceStability.Evolving public void logoutUserFromKeytab() throws IOException { - if (!isSecurityEnabled() || - user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS) { + if (!hasKerberosCredentials()) { return; } - LoginContext login = getLogin(); + HadoopLoginContext login = getLogin(); + String keytabFile = getKeytab(); if (login == null || keytabFile == null) { throw new KerberosAuthException(MUST_FIRST_LOGIN_FROM_KEYTAB); } @@ -1208,9 +987,8 @@ public void logoutUserFromKeytab() throws IOException { if (LOG.isDebugEnabled()) { LOG.debug("Initiating logout for " + getUserName()); } - synchronized (UserGroupInformation.class) { - login.logout(); - } + // hadoop login context internally locks credentials. + login.logout(); } catch (LoginException le) { KerberosAuthException kae = new KerberosAuthException(LOGOUT_FAILURE, le); kae.setUser(user.toString()); @@ -1218,7 +996,7 @@ public void logoutUserFromKeytab() throws IOException { throw kae; } - LOG.info("Logout successful for user " + keytabPrincipal + LOG.info("Logout successful for user " + getUserName() + " using keytab file " + keytabFile); } @@ -1228,18 +1006,8 @@ public void logoutUserFromKeytab() throws IOException { * @throws IOException * @throws KerberosAuthException if it's a kerberos login exception. */ - public synchronized void checkTGTAndReloginFromKeytab() throws IOException { - if (!isSecurityEnabled() - || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS - || !isKeytab) { - return; - } - KerberosTicket tgt = getTGT(); - if (tgt != null && !shouldRenewImmediatelyForTests && - Time.now() < getRefreshTime(tgt)) { - return; - } - reloginFromKeytab(); + public void checkTGTAndReloginFromKeytab() throws IOException { + reloginFromKeytab(true); } // if the first kerberos ticket is not TGT, then remove and destroy it since @@ -1288,63 +1056,26 @@ void fixKerberosTicketOrder() { */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized void reloginFromKeytab() throws IOException { - if (!shouldRelogin() || !isKeytab) { + public void reloginFromKeytab() throws IOException { + reloginFromKeytab(false); + } + + private void reloginFromKeytab(boolean checkTGT) throws IOException { + if (!shouldRelogin() || !isFromKeytab()) { return; } - - long now = Time.now(); - if (!shouldRenewImmediatelyForTests && !hasSufficientTimeElapsed(now)) { - return; - } - - KerberosTicket tgt = getTGT(); - //Return if TGT is valid and is not going to expire soon. - if (tgt != null && !shouldRenewImmediatelyForTests && - now < getRefreshTime(tgt)) { - return; - } - - LoginContext login = getLogin(); - if (login == null || keytabFile == null) { + HadoopLoginContext login = getLogin(); + if (login == null) { throw new KerberosAuthException(MUST_FIRST_LOGIN_FROM_KEYTAB); } - - long start = 0; - // register most recent relogin attempt - user.setLastLogin(now); - try { - if (LOG.isDebugEnabled()) { - LOG.debug("Initiating logout for " + getUserName()); + if (checkTGT) { + KerberosTicket tgt = getTGT(); + if (tgt != null && !shouldRenewImmediatelyForTests && + Time.now() < getRefreshTime(tgt)) { + return; } - synchronized (UserGroupInformation.class) { - // clear up the kerberos state. But the tokens are not cleared! As per - // the Java kerberos login module code, only the kerberos credentials - // are cleared - login.logout(); - // login and also update the subject field of this instance to - // have the new credentials (pass it to the LoginContext constructor) - login = newLoginContext( - HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, getSubject(), - new HadoopConfiguration()); - if (LOG.isDebugEnabled()) { - LOG.debug("Initiating re-login for " + keytabPrincipal); - } - start = Time.now(); - login.login(); - fixKerberosTicketOrder(); - metrics.loginSuccess.add(Time.now() - start); - setLogin(login); - } - } catch (LoginException le) { - if (start > 0) { - metrics.loginFailure.add(Time.now() - start); - } - KerberosAuthException kae = new KerberosAuthException(LOGIN_FAILURE, le); - kae.setPrincipal(keytabPrincipal); - kae.setKeytabFile(keytabFile); - throw kae; } + relogin(login); } /** @@ -1357,14 +1088,31 @@ HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, getSubject(), */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized void reloginFromTicketCache() throws IOException { - if (!shouldRelogin() || !isKrbTkt) { + public void reloginFromTicketCache() throws IOException { + if (!shouldRelogin() || !isFromTicket()) { return; } - LoginContext login = getLogin(); + HadoopLoginContext login = getLogin(); if (login == null) { throw new KerberosAuthException(MUST_FIRST_LOGIN); } + relogin(login); + } + + private void relogin(HadoopLoginContext login) throws IOException { + // ensure the relogin is atomic to avoid leaving credentials in an + // inconsistent state. prevents other ugi instances, SASL, and SPNEGO + // from accessing or altering credentials during the relogin. + synchronized(login.getSubjectLock()) { + // another racing thread may have beat us to the relogin. + if (login == getLogin()) { + unprotectedRelogin(login); + } + } + } + + private void unprotectedRelogin(HadoopLoginContext login) throws IOException { + assert Thread.holdsLock(login.getSubjectLock()); long now = Time.now(); if (!hasSufficientTimeElapsed(now)) { return; @@ -1381,13 +1129,14 @@ public synchronized void reloginFromTicketCache() throws IOException { login.logout(); //login and also update the subject field of this instance to //have the new credentials (pass it to the LoginContext constructor) - login = - newLoginContext(HadoopConfiguration.USER_KERBEROS_CONFIG_NAME, - getSubject(), new HadoopConfiguration()); + login = newLoginContext( + login.getAppName(), login.getSubject(), login.getConfiguration()); if (LOG.isDebugEnabled()) { LOG.debug("Initiating re-login for " + getUserName()); } login.login(); + // this should be unnecessary. originally added due to improper locking + // of the subject during relogin. fixKerberosTicketOrder(); setLogin(login); } catch (LoginException le) { @@ -1405,52 +1154,22 @@ public synchronized void reloginFromTicketCache() throws IOException { * @param path the path to the keytab file * @throws IOException if the keytab file can't be read */ - public synchronized + public static UserGroupInformation loginUserFromKeytabAndReturnUGI(String user, String path ) throws IOException { if (!isSecurityEnabled()) return UserGroupInformation.getCurrentUser(); - String oldKeytabFile = null; - String oldKeytabPrincipal = null; - long start = 0; - try { - oldKeytabFile = keytabFile; - oldKeytabPrincipal = keytabPrincipal; - keytabFile = path; - keytabPrincipal = user; - Subject subject = new Subject(); - - LoginContext login = newLoginContext( - HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, subject, - new HadoopConfiguration()); - - start = Time.now(); - login.login(); - metrics.loginSuccess.add(Time.now() - start); - UserGroupInformation newLoginUser = - new UserGroupInformation(subject, false); - newLoginUser.setLogin(login); - newLoginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS); - - return newLoginUser; - } catch (LoginException le) { - if (start > 0) { - metrics.loginFailure.add(Time.now() - start); - } - KerberosAuthException kae = new KerberosAuthException(LOGIN_FAILURE, le); - kae.setUser(user); - kae.setKeytabFile(path); - throw kae; - } finally { - if(oldKeytabFile != null) keytabFile = oldKeytabFile; - if(oldKeytabPrincipal != null) keytabPrincipal = oldKeytabPrincipal; - } + LoginParams params = new LoginParams(); + params.put(LoginParam.PRINCIPAL, user); + params.put(LoginParam.KEYTAB, path); + return doSubjectLogin(null, params); } private boolean hasSufficientTimeElapsed(long now) { - if (now - user.getLastLogin() < kerberosMinSecondsBeforeRelogin ) { + if (!shouldRenewImmediatelyForTests && + now - user.getLastLogin() < kerberosMinSecondsBeforeRelogin ) { LOG.warn("Not attempting to re-login since the last re-login was " + "attempted less than " + (kerberosMinSecondsBeforeRelogin/1000) + " seconds before. Last Login=" + user.getLastLogin()); @@ -1465,8 +1184,8 @@ private boolean hasSufficientTimeElapsed(long now) { */ @InterfaceAudience.Public @InterfaceStability.Evolving - public synchronized static boolean isLoginKeytabBased() throws IOException { - return getLoginUser().isKeytab; + public static boolean isLoginKeytabBased() throws IOException { + return getLoginUser().isFromKeytab(); } /** @@ -1474,7 +1193,7 @@ public synchronized static boolean isLoginKeytabBased() throws IOException { * @return true or false */ public static boolean isLoginTicketBased() throws IOException { - return getLoginUser().isKrbTkt; + return getLoginUser().isFromTicket(); } /** @@ -1503,7 +1222,7 @@ public static UserGroupInformation createRemoteUser(String user, AuthMethod auth } Subject subject = new Subject(); subject.getPrincipals().add(new User(user)); - UserGroupInformation result = new UserGroupInformation(subject, false); + UserGroupInformation result = new UserGroupInformation(subject); result.setAuthenticationMethod(authMethod); return result; } @@ -1519,7 +1238,7 @@ public enum AuthenticationMethod { SIMPLE(AuthMethod.SIMPLE, HadoopConfiguration.SIMPLE_CONFIG_NAME), KERBEROS(AuthMethod.KERBEROS, - HadoopConfiguration.USER_KERBEROS_CONFIG_NAME), + HadoopConfiguration.KERBEROS_CONFIG_NAME), TOKEN(AuthMethod.TOKEN), CERTIFICATE(null), KERBEROS_SSL(null), @@ -1578,11 +1297,9 @@ public static UserGroupInformation createProxyUser(String user, } Subject subject = new Subject(); Set principals = subject.getPrincipals(); - principals.add(new User(user)); + principals.add(new User(user, AuthenticationMethod.PROXY, null)); principals.add(new RealUser(realUser)); - UserGroupInformation result =new UserGroupInformation(subject, false); - result.setAuthenticationMethod(AuthenticationMethod.PROXY); - return result; + return new UserGroupInformation(subject); } /** @@ -2050,6 +1767,256 @@ private void print() throws IOException { System.out.println(); } + /** + * Login a subject with the given parameters. If the subject is null, + * the login context used to create the subject will be attached. + * @param subject to login, null for new subject. + * @param params for login, null for externally managed ugi. + * @return UserGroupInformation for subject + * @throws IOException + */ + private static UserGroupInformation doSubjectLogin( + Subject subject, LoginParams params) throws IOException { + ensureInitialized(); + // initial default login. + if (subject == null && params == null) { + params = LoginParams.getDefaults(); + } + HadoopConfiguration loginConf = new HadoopConfiguration(params); + try { + HadoopLoginContext login = newLoginContext( + authenticationMethod.getLoginAppName(), subject, loginConf); + login.login(); + UserGroupInformation ugi = new UserGroupInformation(login.getSubject()); + // attach login context for relogin unless this was a pre-existing + // subject. + if (subject == null) { + params.put(LoginParam.PRINCIPAL, ugi.getUserName()); + ugi.setLogin(login); + } + return ugi; + } catch (LoginException le) { + KerberosAuthException kae = + new KerberosAuthException(FAILURE_TO_LOGIN, le); + if (params != null) { + kae.setPrincipal(params.get(LoginParam.PRINCIPAL)); + kae.setKeytabFile(params.get(LoginParam.KEYTAB)); + kae.setTicketCacheFile(params.get(LoginParam.CCACHE)); + } + throw kae; + } + } + + // parameters associated with kerberos logins. may be extended to support + // additional authentication methods. + enum LoginParam { + PRINCIPAL, + KEYTAB, + CCACHE, + } + + // explicitly private to prevent external tampering. + private static class LoginParams extends EnumMap + implements Parameters { + LoginParams() { + super(LoginParam.class); + } + + // do not add null values, nor allow existing values to be overriden. + @Override + public String put(LoginParam param, String val) { + boolean add = val != null && !containsKey(param); + return add ? super.put(param, val) : null; + } + + static LoginParams getDefaults() { + LoginParams params = new LoginParams(); + params.put(LoginParam.PRINCIPAL, System.getenv("KRB5PRINCIPAL")); + params.put(LoginParam.KEYTAB, System.getenv("KRB5KEYTAB")); + params.put(LoginParam.CCACHE, System.getenv("KRB5CCNAME")); + return params; + } + } + + // wrapper to allow access to fields necessary to recreate the same login + // context for relogin. explicitly private to prevent external tampering. + private static class HadoopLoginContext extends LoginContext { + private final String appName; + private final HadoopConfiguration conf; + + HadoopLoginContext(String appName, Subject subject, + HadoopConfiguration conf) throws LoginException { + super(appName, subject, null, conf); + this.appName = appName; + this.conf = conf; + } + + String getAppName() { + return appName; + } + + HadoopConfiguration getConfiguration() { + return conf; + } + + // the locking model for logins cannot rely on ugi instance synchronization + // since a subject will be referenced by multiple ugi instances. + Object getSubjectLock() { + Subject subject = getSubject(); + // if subject is null, the login context will create the subject + // so just lock on this context. + return (subject == null) ? this : subject.getPrivateCredentials(); + } + + @Override + public void login() throws LoginException { + synchronized(getSubjectLock()) { + MutableRate metric = metrics.loginFailure; + long start = Time.monotonicNow(); + try { + super.login(); + metric = metrics.loginSuccess; + } finally { + metric.add(Time.monotonicNow() - start); + } + } + } + + @Override + public void logout() throws LoginException { + synchronized(getSubjectLock()) { + super.logout(); + } + } + } + + /** + * A JAAS configuration that defines the login modules that we want + * to use for login. + */ + @InterfaceAudience.Private + @InterfaceStability.Unstable + private static class HadoopConfiguration + extends javax.security.auth.login.Configuration { + static final String KRB5_LOGIN_MODULE = + KerberosUtil.getKrb5LoginModuleName(); + static final String SIMPLE_CONFIG_NAME = "hadoop-simple"; + static final String KERBEROS_CONFIG_NAME = "hadoop-kerberos"; + + private static final Map BASIC_JAAS_OPTIONS = + new HashMap(); + static { + if ("true".equalsIgnoreCase(System.getenv("HADOOP_JAAS_DEBUG"))) { + BASIC_JAAS_OPTIONS.put("debug", "true"); + } + } + + static final AppConfigurationEntry OS_SPECIFIC_LOGIN = + new AppConfigurationEntry( + OS_LOGIN_MODULE_NAME, + LoginModuleControlFlag.REQUIRED, + BASIC_JAAS_OPTIONS); + + static final AppConfigurationEntry HADOOP_LOGIN = + new AppConfigurationEntry( + HadoopLoginModule.class.getName(), + LoginModuleControlFlag.REQUIRED, + BASIC_JAAS_OPTIONS); + + private final LoginParams params; + + HadoopConfiguration(LoginParams params) { + this.params = params; + } + + @Override + public LoginParams getParameters() { + return params; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String appName) { + ArrayList entries = new ArrayList<>(); + // login of external subject passes no params. technically only + // existing credentials should be used but other components expect + // the login to succeed with local user fallback if no principal. + if (params == null || appName.equals(SIMPLE_CONFIG_NAME)) { + entries.add(OS_SPECIFIC_LOGIN); + } else if (appName.equals(KERBEROS_CONFIG_NAME)) { + // existing semantics are the initial default login allows local user + // fallback. this is not allowed when a principal explicitly + // specified or during a relogin. + if (!params.containsKey(LoginParam.PRINCIPAL)) { + entries.add(OS_SPECIFIC_LOGIN); + } + entries.add(getKerberosEntry()); + } + entries.add(HADOOP_LOGIN); + return entries.toArray(new AppConfigurationEntry[0]); + } + + private AppConfigurationEntry getKerberosEntry() { + final Map options = new HashMap<>(BASIC_JAAS_OPTIONS); + LoginModuleControlFlag controlFlag = LoginModuleControlFlag.OPTIONAL; + // kerberos login is mandatory if principal is specified. principal + // will not be set for initial default login, but will always be set + // for relogins. + final String principal = params.get(LoginParam.PRINCIPAL); + if (principal != null) { + options.put("principal", principal); + controlFlag = LoginModuleControlFlag.REQUIRED; + } + + // use keytab if given else fallback to ticket cache. + if (IBM_JAVA) { + if (params.containsKey(LoginParam.KEYTAB)) { + final String keytab = params.get(LoginParam.KEYTAB); + if (keytab != null) { + options.put("useKeytab", prependFileAuthority(keytab)); + } else { + options.put("useDefaultKeytab", "true"); + } + options.put("credsType", "both"); + } else { + String ticketCache = params.get(LoginParam.CCACHE); + if (ticketCache != null) { + options.put("useCcache", prependFileAuthority(ticketCache)); + } else { + options.put("useDefaultCcache", "true"); + } + options.put("renewTGT", "true"); + } + } else { + if (params.containsKey(LoginParam.KEYTAB)) { + options.put("useKeyTab", "true"); + final String keytab = params.get(LoginParam.KEYTAB); + if (keytab != null) { + options.put("keyTab", keytab); + } + options.put("storeKey", "true"); + } else { + options.put("useTicketCache", "true"); + String ticketCache = params.get(LoginParam.CCACHE); + if (ticketCache != null) { + options.put("ticketCache", ticketCache); + } + options.put("renewTGT", "true"); + } + options.put("doNotPrompt", "true"); + } + options.put("refreshKrb5Config", "true"); + + return new AppConfigurationEntry( + KRB5_LOGIN_MODULE, controlFlag, options); + } + + private static String prependFileAuthority(String keytabPath) { + return keytabPath.startsWith("file://") + ? keytabPath + : "file://" + keytabPath; + } + } + /** * A test method to print out the current user's UGI. * @param args if there are two arguments, read the user from the keytab @@ -2062,7 +2029,7 @@ public static void main(String [] args) throws Exception { ugi.print(); System.out.println("UGI: " + ugi); System.out.println("Auth method " + ugi.user.getAuthenticationMethod()); - System.out.println("Keytab " + ugi.isKeytab); + System.out.println("Keytab " + ugi.isFromKeytab()); System.out.println("============================================================"); if (args.length == 2) { @@ -2070,8 +2037,9 @@ public static void main(String [] args) throws Exception { loginUserFromKeytab(args[0], args[1]); getCurrentUser().print(); System.out.println("Keytab: " + ugi); - System.out.println("Auth method " + loginUser.user.getAuthenticationMethod()); - System.out.println("Keytab " + loginUser.isKeytab); + UserGroupInformation loginUgi = getLoginUser(); + System.out.println("Auth method " + loginUgi.getAuthenticationMethod()); + System.out.println("Keytab " + loginUgi.isFromKeytab()); } } } diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index b5163a1564c..ea26d0699be 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -2946,19 +2946,6 @@ the ZK CLI). - - hadoop.treat.subject.external - false - - When creating UGI with UserGroupInformation(Subject), treat the passed - subject external if set to true, and assume the owner of the subject - should do the credential renewal. - - When true this property will introduce an incompatible change which - may require changes in client code. For more details, see the jiras: - HADOOP-13805,HADOOP-13558. - - hadoop.system.tags YARN,HDFS,NAMENODE,DATANODE,REQUIRED,SECURITY,KERBEROS,PERFORMANCE,CLIENT diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGILoginFromKeytab.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGILoginFromKeytab.java index 61fbf89178c..826e4b2ea08 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGILoginFromKeytab.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGILoginFromKeytab.java @@ -21,14 +21,41 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.minikdc.MiniKdc; +import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedExceptionAction; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.kerberos.KerberosTicket; +import javax.security.auth.login.LoginContext; /** * Verify UGI login from keytab. Check that the UGI is @@ -39,6 +66,7 @@ public class TestUGILoginFromKeytab { private MiniKdc kdc; private File workDir; + private ExecutorService executor; @Rule public final TemporaryFolder folder = new TemporaryFolder(); @@ -51,9 +79,12 @@ public void startMiniKdc() throws Exception { conf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); UserGroupInformation.setConfiguration(conf); + UserGroupInformation.setShouldRenewImmediatelyForTests(true); workDir = folder.getRoot(); kdc = new MiniKdc(MiniKdc.createConf(), workDir); kdc.start(); + executor = Executors.newCachedThreadPool(); + } @After @@ -61,6 +92,9 @@ public void stopMiniKdc() { if (kdc != null) { kdc.stop(); } + if (executor != null) { + executor.shutdownNow(); + } } /** @@ -69,7 +103,6 @@ public void stopMiniKdc() { */ @Test public void testUGILoginFromKeytab() throws Exception { - UserGroupInformation.setShouldRenewImmediatelyForTests(true); String principal = "foo"; File keytab = new File(workDir, "foo.keytab"); kdc.createPrincipal(keytab, principal); @@ -80,12 +113,379 @@ public void testUGILoginFromKeytab() throws Exception { ugi.isFromKeytab()); // Verify relogin from keytab. - User user = ugi.getSubject().getPrincipals(User.class).iterator().next(); + User user = getUser(ugi.getSubject()); final long firstLogin = user.getLastLogin(); + final LoginContext login1 = user.getLogin(); + Assert.assertNotNull(login1); + ugi.reloginFromKeytab(); final long secondLogin = user.getLastLogin(); + final LoginContext login2 = user.getLogin(); Assert.assertTrue("User should have been able to relogin from keytab", secondLogin > firstLogin); + Assert.assertNotNull(login2); + Assert.assertNotSame(login1, login2); } + @Test + public void testGetUGIFromKnownSubject() throws Exception { + KerberosPrincipal principal = new KerberosPrincipal("user"); + File keytab = new File(workDir, "user.keytab"); + kdc.createPrincipal(keytab, principal.getName()); + + UserGroupInformation ugi1 = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal.getName(), keytab.getPath()); + Subject subject = ugi1.getSubject(); + User user = getUser(subject); + Assert.assertNotNull(user); + LoginContext login = user.getLogin(); + Assert.assertNotNull(login); + + // User instance and/or login context should not change. + UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject); + Assert.assertSame(user, getUser(ugi2.getSubject())); + Assert.assertSame(login, user.getLogin()); + } + + @Test + public void testGetUGIFromExternalSubject() throws Exception { + KerberosPrincipal principal = new KerberosPrincipal("user"); + File keytab = new File(workDir, "user.keytab"); + kdc.createPrincipal(keytab, principal.getName()); + + UserGroupInformation ugi = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal.getName(), keytab.getPath()); + Subject subject = ugi.getSubject(); + removeUser(subject); + + // first call to get the ugi should add the User instance w/o a login + // context. + UserGroupInformation ugi1 = UserGroupInformation.getUGIFromSubject(subject); + Assert.assertSame(subject, ugi1.getSubject()); + User user = getUser(subject); + Assert.assertNotNull(user); + Assert.assertEquals(principal.getName(), user.getName()); + Assert.assertNull(user.getLogin()); + + // subsequent call should not change the existing User instance. + UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject); + Assert.assertSame(subject, ugi2.getSubject()); + Assert.assertSame(user, getUser(ugi2.getSubject())); + Assert.assertNull(user.getLogin()); + } + + @Test + public void testGetUGIFromExternalSubjectWithLogin() throws Exception { + KerberosPrincipal principal = new KerberosPrincipal("user"); + File keytab = new File(workDir, "user.keytab"); + kdc.createPrincipal(keytab, principal.getName()); + + // alter the User's login context to appear to be a foreign and + // unmanagable context. + UserGroupInformation ugi = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal.getName(), keytab.getPath()); + Subject subject = ugi.getSubject(); + User user = getUser(subject); + final LoginContext dummyLogin = Mockito.mock(LoginContext.class); + user.setLogin(dummyLogin); + + // nothing should change. + UserGroupInformation ugi2 = UserGroupInformation.getUGIFromSubject(subject); + Assert.assertSame(subject, ugi2.getSubject()); + Assert.assertSame(user, getUser(ugi2.getSubject())); + Assert.assertSame(dummyLogin, user.getLogin()); + } + + + private static KerberosTicket getTicket(UserGroupInformation ugi) { + Set tickets = + ugi.getSubject().getPrivateCredentials(KerberosTicket.class); + return tickets.isEmpty() ? null : tickets.iterator().next(); + } + + // verify ugi has expected principal, a keytab, and has a ticket for + // the expected principal. + private static KerberosTicket checkTicketAndKeytab(UserGroupInformation ugi, + KerberosPrincipal principal, boolean expectIsKeytab) { + Assert.assertEquals("wrong principal", + principal.getName(), ugi.getUserName()); + Assert.assertEquals("is not keytab", + expectIsKeytab, ugi.isFromKeytab()); + KerberosTicket ticket = getTicket(ugi); + Assert.assertNotNull("no ticket", ticket); + Assert.assertEquals("wrong principal", principal, ticket.getClient()); + return ticket; + } + + @Test + public void testReloginForUGIFromSubject() throws Exception { + KerberosPrincipal principal1 = new KerberosPrincipal("user1"); + File keytab1 = new File(workDir, "user1.keytab"); + kdc.createPrincipal(keytab1, principal1.getName()); + + KerberosPrincipal principal2 = new KerberosPrincipal("user2"); + File keytab2 = new File(workDir, "user2.keytab"); + kdc.createPrincipal(keytab2, principal2.getName()); + + // Login a user and remove the User instance so it looks like an + // "external" subject. + final Subject extSubject = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal2.getName(), keytab2.getPath()).getSubject(); + removeUser(extSubject); + + // Login another user. + UserGroupInformation.loginUserFromKeytab( + principal1.getName(), keytab1.getPath()); + final UserGroupInformation loginUser = UserGroupInformation.getLoginUser(); + + loginUser.doAs(new PrivilegedExceptionAction() { + @Override + public Void run() throws IOException { + KerberosTicket loginTicket = + checkTicketAndKeytab(loginUser, principal1, true); + + // get the ugi for the previously logged in subject. + UserGroupInformation extSubjectUser = + UserGroupInformation.getUGIFromSubject(extSubject); + KerberosTicket ticket = + checkTicketAndKeytab(extSubjectUser, principal2, false); + + // verify login user got a new ticket. + loginUser.reloginFromKeytab(); + KerberosTicket newLoginTicket = + checkTicketAndKeytab(loginUser, principal1, true); + Assert.assertNotEquals(loginTicket.getAuthTime(), + newLoginTicket.getAuthTime()); + + // verify an "external" subject ticket does not change. + extSubjectUser.reloginFromKeytab(); + Assert.assertSame(ticket, + checkTicketAndKeytab(extSubjectUser, principal2, false)); + + // verify subject ugi relogin did not affect the login user. + Assert.assertSame(newLoginTicket, + checkTicketAndKeytab(loginUser, principal1, true)); + + return null; + } + }); + } + + @Test + public void testReloginForLoginFromSubject() throws Exception { + KerberosPrincipal principal1 = new KerberosPrincipal("user1"); + File keytab1 = new File(workDir, "user1.keytab"); + kdc.createPrincipal(keytab1, principal1.getName()); + + KerberosPrincipal principal2 = new KerberosPrincipal("user2"); + File keytab2 = new File(workDir, "user2.keytab"); + kdc.createPrincipal(keytab2, principal2.getName()); + + // login principal1 with a keytab. + UserGroupInformation.loginUserFromKeytab( + principal1.getName(), keytab1.getPath()); + final UserGroupInformation originalLoginUser = + UserGroupInformation.getLoginUser(); + Assert.assertNotNull(getUser(originalLoginUser.getSubject()).getLogin()); + + originalLoginUser.doAs(new PrivilegedExceptionAction() { + @Override + public Void run() throws IOException { + KerberosTicket originalLoginUserTicket = + checkTicketAndKeytab(originalLoginUser, principal1, true); + + // login principal2 from a subject with keytab. it's external so + // no login context should be attached to the user. + final Subject subject = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal2.getName(), keytab2.getPath()).getSubject(); + removeUser(subject); + + // verify the new login user is external. + UserGroupInformation.loginUserFromSubject(subject); + Assert.assertNull(getUser(subject).getLogin()); + UserGroupInformation extLoginUser = + UserGroupInformation.getLoginUser(); + KerberosTicket extLoginUserTicket = + checkTicketAndKeytab(extLoginUser, principal2, false); + + // verify subject-based login user does not get a new ticket, and + // original login user not affected. + extLoginUser.reloginFromKeytab(); + Assert.assertSame(extLoginUserTicket, + checkTicketAndKeytab(extLoginUser, principal2, false)); + Assert.assertSame(originalLoginUserTicket, + checkTicketAndKeytab(originalLoginUser, principal1, true)); + + // verify original login user gets a new ticket, new login user + // not affected. + originalLoginUser.reloginFromKeytab(); + Assert.assertNotSame(originalLoginUserTicket, + checkTicketAndKeytab(originalLoginUser, principal1, true)); + Assert.assertSame(extLoginUserTicket, + checkTicketAndKeytab(extLoginUser, principal2, false)); + return null; + } + }); + } + + @Test + public void testReloginAfterFailedRelogin() throws Exception { + KerberosPrincipal principal = new KerberosPrincipal("user1"); + File keytab = new File(workDir, "user1.keytab"); + File keytabBackup = new File(keytab + ".backup"); + kdc.createPrincipal(keytab, principal.getName()); + + UserGroupInformation.loginUserFromKeytab( + principal.getName(), keytab.getPath()); + final UserGroupInformation loginUser = UserGroupInformation.getLoginUser(); + checkTicketAndKeytab(loginUser, principal, true); + + // move the keytab to induce a relogin failure. + Assert.assertTrue(keytab.renameTo(keytabBackup)); + try { + loginUser.reloginFromKeytab(); + Assert.fail("relogin should fail"); + } catch (KerberosAuthException kae) { + // expected. + } + + // even though no KeyTab object, ugi should know it's keytab based. + Assert.assertTrue(loginUser.isFromKeytab()); + Assert.assertNull(getTicket(loginUser)); + + // move keytab back to enable relogin to succeed. + Assert.assertTrue(keytabBackup.renameTo(keytab)); + loginUser.reloginFromKeytab(); + checkTicketAndKeytab(loginUser, principal, true); + } + + // verify getting concurrent relogins blocks to avoid indeterminate + // credentials corruption, but getting a ugi for the subject does not block. + @Test(timeout=180000) + public void testConcurrentRelogin() throws Exception { + final CyclicBarrier barrier = new CyclicBarrier(2); + final CountDownLatch latch = new CountDownLatch(1); + assertTrue(UserGroupInformation.isSecurityEnabled()); + + KerberosPrincipal principal = new KerberosPrincipal("testUser"); + File keytab = new File(workDir, "user1.keytab"); + kdc.createPrincipal(keytab, principal.getName()); + + // create a keytab ugi. + final UserGroupInformation loginUgi = + UserGroupInformation.loginUserFromKeytabAndReturnUGI( + principal.getName(), keytab.getPath()); + assertEquals(AuthenticationMethod.KERBEROS, + loginUgi.getAuthenticationMethod()); + assertTrue(loginUgi.isFromKeytab()); + + // create a new ugi instance based on subject from the logged in user. + final UserGroupInformation clonedUgi = + UserGroupInformation.getUGIFromSubject(loginUgi.getSubject()); + assertEquals(AuthenticationMethod.KERBEROS, + clonedUgi.getAuthenticationMethod()); + assertTrue(clonedUgi.isFromKeytab()); + + // cause first relogin to block on a barrier in logout to verify relogins + // are atomic. + User user = getUser(loginUgi.getSubject()); + final LoginContext spyLogin = Mockito.spy(user.getLogin()); + user.setLogin(spyLogin); + Mockito.doAnswer(new Answer(){ + @Override + public Void answer(InvocationOnMock invocation) + throws Throwable { + invocation.callRealMethod(); + latch.countDown(); + barrier.await(); + return null; + } + }).when(spyLogin).logout(); + + Future relogin = executor.submit( + new Callable(){ + @Override + public Void call() throws Exception { + Thread.currentThread().setName("relogin"); + loginUgi.reloginFromKeytab(); + return null; + } + }); + // wait for the thread to block on the barrier in the logout of the + // relogin. + assertTrue("first relogin didn't block", + latch.await(2, TimeUnit.SECONDS)); + + // although the logout removed the keytab instance, verify the ugi + // knows from its login params that it is supposed to be from a keytab. + assertTrue(clonedUgi.isFromKeytab()); + + // another concurrent re-login should block. + Mockito.doNothing().when(spyLogin).logout(); + Mockito.doNothing().when(spyLogin).login(); + Future clonedRelogin = executor.submit( + new Callable(){ + @Override + public UserGroupInformation call() throws Exception { + Thread.currentThread().setName("clonedRelogin"); + clonedUgi.reloginFromKeytab(); + return clonedUgi; + } + }); + + try { + clonedRelogin.get(2, TimeUnit.SECONDS); + fail("second relogin didn't block!"); + } catch (TimeoutException te) { + // expected + } + + // concurrent UGI instantiation should not block and again should + // know it's supposed to be from a keytab. + loginUgi.doAs(new PrivilegedExceptionAction(){ + @Override + public Void run() throws Exception { + UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); + assertEquals(principal.getName(), ugi.getUserName()); + assertTrue(ugi.isFromKeytab()); + return null; + } + }); + clonedUgi.doAs(new PrivilegedExceptionAction(){ + @Override + public Void run() throws Exception { + UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); + assertEquals(principal.getName(), ugi.getUserName()); + assertTrue(ugi.isFromKeytab()); + return null; + } + }); + + // second relogin should still be blocked until the original relogin + // is blocked. + assertFalse(clonedRelogin.isDone()); + barrier.await(); + relogin.get(); + clonedRelogin.get(); + } + + private User getUser(Subject subject) { + Iterator iter = subject.getPrincipals(User.class).iterator(); + return iter.hasNext() ? iter.next() : null; + } + + private void removeUser(Subject subject) { + // remove User instance so it appears to not be logged in. + for (Iterator iter = subject.getPrincipals().iterator(); + iter.hasNext(); ) { + if (iter.next() instanceof User) { + iter.remove(); + } + } + } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java index 6c94b1da648..de74d178636 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java @@ -58,9 +58,11 @@ public void teardown() { private void setupKdc() throws Exception { Properties kdcConf = MiniKdc.createConf(); - // tgt expire time = 30 seconds - kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, "30"); - kdcConf.setProperty(MiniKdc.MIN_TICKET_LIFETIME, "30"); + // tgt expire time = 2 seconds. just testing that renewal thread retries + // for expiring tickets, so no need to waste time waiting for expiry to + // arrive. + kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, "2"); + kdcConf.setProperty(MiniKdc.MIN_TICKET_LIFETIME, "2"); File kdcDir = new File(System.getProperty("test.dir", "target")); kdc = new MiniKdc(kdcConf, kdcDir); kdc.start(); @@ -70,12 +72,14 @@ private void setupKdc() throws Exception { public void testAutoRenewalThreadRetryWithKdc() throws Exception { GenericTestUtils.setLogLevel(UserGroupInformation.LOG, Level.DEBUG); final Configuration conf = new Configuration(); + // can't rely on standard kinit, else test fails when user running + // the test is kinit'ed because the test renews _their TGT_. + conf.set("hadoop.kerberos.kinit.command", "bogus-kinit-cmd"); // Relogin every 1 second conf.setLong(HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN, 1); SecurityUtil.setAuthenticationMethod( UserGroupInformation.AuthenticationMethod.KERBEROS, conf); UserGroupInformation.setConfiguration(conf); - UserGroupInformation.setEnableRenewThreadCreationForTest(true); LoginContext loginContext = null; try { @@ -87,44 +91,10 @@ public void testAutoRenewalThreadRetryWithKdc() throws Exception { setupKdc(); kdc.createPrincipal(keytab, principal); - // client login - final Subject subject = - new Subject(false, principals, new HashSet<>(), new HashSet<>()); - - loginContext = new LoginContext("", subject, null, - new javax.security.auth.login.Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry( - String name) { - Map options = new HashMap<>(); - options.put("principal", principal); - options.put("refreshKrb5Config", "true"); - if (PlatformName.IBM_JAVA) { - options.put("useKeytab", keytab.getPath()); - options.put("credsType", "both"); - } else { - options.put("keyTab", keytab.getPath()); - options.put("useKeyTab", "true"); - options.put("storeKey", "true"); - options.put("doNotPrompt", "true"); - options.put("useTicketCache", "true"); - options.put("renewTGT", "true"); - options.put("isInitiator", Boolean.toString(true)); - } - String ticketCache = System.getenv("KRB5CCNAME"); - if (ticketCache != null) { - options.put("ticketCache", ticketCache); - } - options.put("debug", "true"); - return new AppConfigurationEntry[] {new AppConfigurationEntry( - KerberosUtil.getKrb5LoginModuleName(), - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, - options)}; - } - }); - loginContext.login(); - final Subject loginSubject = loginContext.getSubject(); - UserGroupInformation.loginUserFromSubject(loginSubject); + UserGroupInformation.loginUserFromKeytab(principal, keytab.getPath()); + UserGroupInformation ugi = UserGroupInformation.getLoginUser(); + // no ticket cache, so force the thread to test for failures. + ugi.spawnAutoRenewalThreadForUserCreds(true); // Verify retry happens. Do not verify retry count to reduce flakiness. // Detailed back-off logic is tested separately in diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUserGroupInformation.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUserGroupInformation.java index bcb2126c061..94779901fdb 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUserGroupInformation.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUserGroupInformation.java @@ -38,6 +38,9 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -53,15 +56,19 @@ import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Method; +import java.security.Principal; import java.security.PrivilegedExceptionAction; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Date; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; - -import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_TREAT_SUBJECT_EXTERNAL_KEY; import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_KERBEROS_MIN_SECONDS_BEFORE_RELOGIN; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTH_TO_LOCAL; @@ -112,7 +119,14 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { throw new RuntimeException("UGI is not using its own security conf!"); } } - + + // must be set immediately to avoid inconsistent testing issues. + static { + // fake the realm is kerberos is enabled + System.setProperty("java.security.krb5.kdc", ""); + System.setProperty("java.security.krb5.realm", "DEFAULT.REALM"); + } + /** configure ugi */ @BeforeClass public static void setup() { @@ -123,9 +137,6 @@ public static void setup() { // that finds winutils.exe String home = System.getenv("HADOOP_HOME"); System.setProperty("hadoop.home.dir", (home != null ? home : ".")); - // fake the realm is kerberos is enabled - System.setProperty("java.security.krb5.kdc", ""); - System.setProperty("java.security.krb5.realm", "DEFAULT.REALM"); } @Before @@ -1021,7 +1032,8 @@ public void testExternalTokenFiles() throws Exception { assertTrue(credsugiTokens.contains(token2)); } - private void testCheckTGTAfterLoginFromSubjectHelper() throws Exception { + @Test + public void testCheckTGTAfterLoginFromSubject() throws Exception { // security on, default is remove default realm SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf); UserGroupInformation.setConfiguration(conf); @@ -1043,17 +1055,6 @@ public Void run() throws IOException { }); } - @Test(expected = KerberosAuthException.class) - public void testCheckTGTAfterLoginFromSubject() throws Exception { - testCheckTGTAfterLoginFromSubjectHelper(); - } - - @Test - public void testCheckTGTAfterLoginFromSubjectFix() throws Exception { - conf.setBoolean(HADOOP_TREAT_SUBJECT_EXTERNAL_KEY, true); - testCheckTGTAfterLoginFromSubjectHelper(); - } - @Test public void testGetNextRetryTime() throws Exception { GenericTestUtils.setLogLevel(UserGroupInformation.LOG, Level.DEBUG); @@ -1134,4 +1135,80 @@ private void assertWithinBounds(final int numFailures, final long lastRetry, LOG.info(str); assertTrue(str, lower <= lastRetry && lastRetry < upper); } + + // verify that getCurrentUser on the same and different subjects can be + // concurrent. Ie. no synchronization. + @Test(timeout=8000) + public void testConcurrentGetCurrentUser() throws Exception { + final CyclicBarrier barrier = new CyclicBarrier(2); + final CountDownLatch latch = new CountDownLatch(1); + + final UserGroupInformation testUgi1 = + UserGroupInformation.createRemoteUser("testUgi1"); + + final UserGroupInformation testUgi2 = + UserGroupInformation.createRemoteUser("testUgi2"); + + // swap the User with a spy to allow getCurrentUser to block when the + // spy is called for the user name. + Set principals = testUgi1.getSubject().getPrincipals(); + User user = + testUgi1.getSubject().getPrincipals(User.class).iterator().next(); + final User spyUser = Mockito.spy(user); + principals.remove(user); + principals.add(spyUser); + when(spyUser.getName()).thenAnswer(new Answer(){ + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + barrier.await(); + return (String)invocation.callRealMethod(); + } + }); + // wait for the thread to block on the barrier in getCurrentUser. + Future blockingLookup = + Executors.newSingleThreadExecutor().submit( + new Callable(){ + @Override + public UserGroupInformation call() throws Exception { + return testUgi1.doAs( + new PrivilegedExceptionAction() { + @Override + public UserGroupInformation run() throws Exception { + return UserGroupInformation.getCurrentUser(); + } + }); + } + }); + latch.await(); + + // old versions of mockito synchronize on returning mocked answers so + // the blocked getCurrentUser will block all other calls to getName. + // workaround this by swapping out the spy with the original User. + principals.remove(spyUser); + principals.add(user); + // concurrent getCurrentUser on ugi1 should not be blocked. + UserGroupInformation ugi; + ugi = testUgi1.doAs( + new PrivilegedExceptionAction() { + @Override + public UserGroupInformation run() throws Exception { + return UserGroupInformation.getCurrentUser(); + } + }); + assertSame(testUgi1.getSubject(), ugi.getSubject()); + // concurrent getCurrentUser on ugi2 should not be blocked. + ugi = testUgi2.doAs( + new PrivilegedExceptionAction() { + @Override + public UserGroupInformation run() throws Exception { + return UserGroupInformation.getCurrentUser(); + } + }); + assertSame(testUgi2.getSubject(), ugi.getSubject()); + + // unblock the original call. + barrier.await(); + assertSame(testUgi1.getSubject(), blockingLookup.get().getSubject()); + } }