You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by om...@apache.org on 2011/03/04 05:06:54 UTC

svn commit: r1077358 - in /hadoop/common/branches/branch-0.20-security-patches/src: core/org/apache/hadoop/ipc/ core/org/apache/hadoop/security/ test/org/apache/hadoop/security/

Author: omalley
Date: Fri Mar  4 04:06:53 2011
New Revision: 1077358

URL: http://svn.apache.org/viewvc?rev=1077358&view=rev
Log:
commit 747360d53aa584c89d7ddc517b7b34832d547f31
Author: Owen O'Malley <om...@apache.org>
Date:   Tue Mar 23 00:20:20 2010 -0700

    HADOOP-6656. Renew Kerberos TGT when 80% of the renew lifetime has been
    used up. (omalley)
    
    +++ b/YAHOO-CHANGES.txt
    +    HADOOP-6656. Renew Kerberos TGT when 80% of the renew lifetime has been
    +    used up. (omalley)
    +

Modified:
    hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/ipc/Client.java
    hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/User.java
    hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/UserGroupInformation.java
    hadoop/common/branches/branch-0.20-security-patches/src/test/org/apache/hadoop/security/TestUserGroupInformation.java

Modified: hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/ipc/Client.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/ipc/Client.java?rev=1077358&r1=1077357&r2=1077358&view=diff
==============================================================================
--- hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/ipc/Client.java (original)
+++ hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/ipc/Client.java Fri Mar  4 04:06:53 2011
@@ -360,17 +360,21 @@ public class Client {
         return saslRpcClient.saslConnect(in2, out2);
       } catch (javax.security.sasl.SaslException je) {
         UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
-        UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
+        UserGroupInformation currentUser = 
+          UserGroupInformation.getCurrentUser();
         UserGroupInformation realUser = currentUser.getRealUser();
         if (authMethod == AuthMethod.KERBEROS && 
-            UserGroupInformation.isLoginKeytabBased() &&
             // relogin only in case it is the login user (e.g. JT)
             // or superuser (like oozie).
             (currentUser.equals(loginUser) || loginUser.equals(realUser))) {
           //try setting up the connection again
           try {
             //try re-login
-            loginUser.reloginFromKeytab();
+            if (UserGroupInformation.isLoginKeytabBased()) {
+              loginUser.reloginFromKeytab();
+            } else {
+              loginUser.reloginFromTicketCache();
+            }
             disposeSasl();
             saslRpcClient = new SaslRpcClient(authMethod, token,
                 serverPrincipal);

Modified: hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/User.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/User.java?rev=1077358&r1=1077357&r2=1077358&view=diff
==============================================================================
--- hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/User.java (original)
+++ hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/User.java Fri Mar  4 04:06:53 2011
@@ -33,6 +33,7 @@ class User implements Principal {
   private final String shortName;
   private AuthenticationMethod authMethod = null;
   private LoginContext login = null;
+  private long lastLogin = 0;
 
   public User(String name) {
     this(name, null, null);
@@ -109,4 +110,20 @@ class User implements Principal {
   public void setLogin(LoginContext login) {
     this.login = login;
   }
+  
+  /**
+   * Set the last login time.
+   * @param time the number of milliseconds since the beginning of time
+   */
+  public void setLastLogin(long time) {
+    lastLogin = time;
+  }
+  
+  /**
+   * Get the time of the last login.
+   * @return the number of milliseconds since the beginning of time.
+   */
+  public long getLastLogin() {
+    return lastLogin;
+  }
 }

Modified: hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/UserGroupInformation.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/UserGroupInformation.java?rev=1077358&r1=1077357&r2=1077358&view=diff
==============================================================================
--- hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/UserGroupInformation.java (original)
+++ hadoop/common/branches/branch-0.20-security-patches/src/core/org/apache/hadoop/security/UserGroupInformation.java Fri Mar  4 04:06:53 2011
@@ -38,7 +38,9 @@ import java.util.Set;
 
 import javax.security.auth.Subject;
 import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.kerberos.KerberosKey;
 import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KerberosTicket;
 import javax.security.auth.login.AppConfigurationEntry;
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
@@ -49,9 +51,9 @@ import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.Path;
-import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
 import org.apache.hadoop.security.token.Token;
 import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.hadoop.util.Shell;
 
 import com.sun.security.auth.NTUserPrincipal;
 import com.sun.security.auth.UnixPrincipal;
@@ -65,6 +67,10 @@ import com.sun.security.auth.module.Krb5
  */
 public class UserGroupInformation {
   private static final Log LOG =  LogFactory.getLog(UserGroupInformation.class);
+  /**
+   * Percentage of the ticket window to use before we renew ticket.
+   */
+  private static final float TICKET_RENEW_WINDOW = 0.80f;
   
   /**
    * A login module that looks at the Kerberos, Unix, or Windows principal and
@@ -131,15 +137,13 @@ public class UserGroupInformation {
   /** The configuration to use */
   private static Configuration conf;
   
-  public static final long MIN_TIME_BEFORE_RELOGIN = 10 * 60 * 1000L;
+  /** Leave 10 minutes between relogin attempts. */
+  private static final long MIN_TIME_BEFORE_RELOGIN = 10 * 60 * 1000L;
   
   /**Environment variable pointing to the token cache file*/
   public static final String HADOOP_TOKEN_FILE_LOCATION = 
     "HADOOP_TOKEN_FILE_LOCATION";
   
-  /** The last relogin attempt */
-  private long lastReloginTime = 0;
-  
   /** 
    * A method to initialize the fields that depend on a configuration.
    * Must be called before useKerberos or groups is used.
@@ -214,6 +218,9 @@ public class UserGroupInformation {
   private static String keytabFile = null;
 
   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 static final String OS_LOGIN_MODULE_NAME;
   private static final Class<? extends Principal> OS_PRINCIPAL_CLASS;
@@ -339,16 +346,11 @@ public class UserGroupInformation {
   }
   
   private LoginContext getLogin() {
-    for (User p: subject.getPrincipals(User.class)) {
-      return p.getLogin();
-    }
-    return null;
+    return user.getLogin();
   }
   
   private void setLogin(LoginContext login) {
-    for (User p: subject.getPrincipals(User.class)) {
-      p.setLogin(login);
-    }
+    user.setLogin(login);
   }
 
   /**
@@ -358,6 +360,8 @@ public class UserGroupInformation {
    */
   UserGroupInformation(Subject subject) {
     this.subject = subject;
+    this.user = subject.getPrincipals(User.class).iterator().next();
+    this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
   }
 
   /**
@@ -381,7 +385,6 @@ public class UserGroupInformation {
     if (loginUser == null) {
       try {
         Subject subject = new Subject();
-        loginUser = new UserGroupInformation(subject);
         LoginContext login;
         if (isSecurityEnabled()) {
           login = new LoginContext(HadoopConfiguration.USER_KERBEROS_CONFIG_NAME, subject);
@@ -389,7 +392,11 @@ public class UserGroupInformation {
           login = new LoginContext(HadoopConfiguration.SIMPLE_CONFIG_NAME, subject);
         }
         login.login();
+        loginUser = new UserGroupInformation(subject);
         loginUser.setLogin(login);
+        loginUser.setAuthenticationMethod(isSecurityEnabled() ?
+                                          AuthenticationMethod.KERBEROS :
+                                          AuthenticationMethod.SIMPLE);
         loginUser = new UserGroupInformation(login.getSubject());
         String fileLocation = System.getenv(HADOOP_TOKEN_FILE_LOCATION);
         if (fileLocation != null && isSecurityEnabled()) {
@@ -401,6 +408,7 @@ public class UserGroupInformation {
             loginUser.addToken(token);
           }
         }
+        loginUser.spawnAutoRenewalThreadForUserCreds();
       } catch (LoginException le) {
         throw new IOException("failure to login", le);
       }
@@ -409,6 +417,90 @@ public class UserGroupInformation {
   }
 
   /**
+   * Is this user logged in from a keytab file?
+   * @return true if the credentials are from a keytab file.
+   */
+  public boolean isFromKeytab() {
+    return isKeytab;
+  }
+
+  /**Spawn a thread to do periodic renewals of kerberos credentials*/
+  private void spawnAutoRenewalThreadForUserCreds() {
+    if (isSecurityEnabled()) {
+      //spawn thread only if we have kerb credentials
+      if (user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS &&
+          !isKeytab) {
+        Thread t = new Thread(new Runnable() {
+          
+          /**
+           * Get the Kerberos TGT
+           * @return the user's TGT or null if none was found
+           */
+          private KerberosTicket getTGT() {
+            Set<KerberosTicket> tickets = 
+              subject.getPrivateCredentials(KerberosTicket.class);
+            for(KerberosTicket ticket: tickets) {
+              KerberosPrincipal server = ticket.getServer();
+              if (server.getName().equals("krbtgt/" + server.getRealm() + 
+                                          "@" + server.getRealm())) {
+                if (LOG.isDebugEnabled()) {
+                  LOG.debug("Found tgt " + ticket);
+                }
+                return ticket;
+              }
+            }
+            return null;
+          }
+
+          private long getRefreshTime(KerberosTicket tgt) {
+            long start = tgt.getStartTime().getTime();
+            long end = tgt.getEndTime().getTime();
+            return start + (long) ((end - start) * TICKET_RENEW_WINDOW);
+          }
+
+          public void run() {
+            String cmd = conf.get("hadoop.kerberos.kinit.command",
+                                  "/usr/kerberos/bin/kinit");
+            KerberosTicket tgt = getTGT();
+            if (tgt == null) {
+              return;
+            }
+            long nextRefresh = getRefreshTime(tgt);
+            while (true) {
+              try {
+                long now = System.currentTimeMillis();
+                LOG.debug("Current time is " + now);
+                LOG.debug("Next refresh is " + nextRefresh);
+                if (now < nextRefresh) {
+                  Thread.sleep(nextRefresh - now);
+                }
+                Shell.execCommand(cmd, "-R");
+                LOG.debug("renewed ticket");
+                reloginFromTicketCache();
+                tgt = getTGT();
+                if (tgt == null) {
+                  LOG.warn("No TGT after renewal. Aborting renew thread for " +
+                           getUserName());
+                }
+                nextRefresh = Math.max(getRefreshTime(tgt),
+                                       now + MIN_TIME_BEFORE_RELOGIN);
+              } catch (InterruptedException ie) {
+                LOG.warn("Terminating renewal thread");
+                return;
+              } catch (IOException ie) {
+                LOG.warn("Exception encountered while running the" +
+                    " renewal command (but continuing)" + ie);
+              }
+            }
+          }
+        });
+        t.setDaemon(true);
+        t.setName("TGT Renewer for " + getUserName());
+        t.start();
+      }
+    }
+  }
+  /**
    * Log a user in from a keytab file. Loads a user identity from a keytab
    * file and login them in. They become the currently logged-in user.
    * @param user the principal name to load from the keytab
@@ -432,6 +524,7 @@ public class UserGroupInformation {
       login.login();
       loginUser = new UserGroupInformation(subject);
       loginUser.setLogin(login);
+      loginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS);
     } catch (LoginException le) {
       throw new IOException("Login failure for " + user + " from keytab " + 
                             path, le);
@@ -440,6 +533,46 @@ public class UserGroupInformation {
         + " using keytab file " + keytabFile);
   }
   /**
+   * Re-Login a user in from the ticket cache.  This
+   * method assumes that login had happened already.
+   * The Subject field of this UserGroupInformation object is updated to have
+   * the new credentials.
+   * @throws IOException on a failure
+   */
+  public synchronized void reloginFromTicketCache()
+  throws IOException {
+    if (!isSecurityEnabled() || 
+        user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
+        isKeytab)
+      return;
+    LoginContext login = getLogin();
+    if (login == null) {
+      throw new IOException("login must be done first");
+    }
+    if (!hasSufficientTimeElapsed()) {
+      return;
+    }
+    try {
+      LOG.info("Initiating logout for " + getUserName());
+      //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 = 
+        new LoginContext(HadoopConfiguration.USER_KERBEROS_CONFIG_NAME, 
+            getSubject());
+      LOG.info("Initiating re-login for " + getUserName());
+      login.login();
+      setLogin(login);
+    } catch (LoginException le) {
+      throw new IOException("Login failure for " + getUserName(), le);
+    } 
+  }
+
+
+  /**
    * Log a user in from a keytab file. Loads a user identity from a keytab
    * file and login them in. This new user does not affect the currently
    * logged-in user.
@@ -468,6 +601,7 @@ public class UserGroupInformation {
       login.login();
       UserGroupInformation newLoginUser = new UserGroupInformation(subject);
       newLoginUser.setLogin(login);
+      newLoginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS);
       
       return newLoginUser;
     } catch (LoginException le) {
@@ -490,21 +624,17 @@ public class UserGroupInformation {
    */
   public synchronized void reloginFromKeytab()
   throws IOException {
-    if (!isSecurityEnabled())
+    if (!isSecurityEnabled() || 
+        user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
+        !isKeytab)
       return;
     LoginContext login = getLogin();
     if (login == null || keytabFile == null) {
       throw new IOException("loginUserFromKeyTab must be done first");
     }
-    long now = System.currentTimeMillis();
-    if (now - lastReloginTime < MIN_TIME_BEFORE_RELOGIN ) {
-      LOG.warn("Not attempting to re-login since the last re-login was " +
-          "attempted less than " + (MIN_TIME_BEFORE_RELOGIN/1000) + " seconds"+
-          " before.");
+    if (!hasSufficientTimeElapsed()) {
       return;
     }
-    // register most recent relogin
-    lastReloginTime = System.currentTimeMillis();
     try {
       LOG.info("Initiating logout for " + getUserName());
       //clear up the kerberos state. But the tokens are not cleared! As per 
@@ -525,9 +655,27 @@ public class UserGroupInformation {
     } 
   }
 
+  private boolean hasSufficientTimeElapsed() {
+    long now = System.currentTimeMillis();
+    if (now - user.getLastLogin() < MIN_TIME_BEFORE_RELOGIN ) {
+      LOG.warn("Not attempting to re-login since the last re-login was " +
+          "attempted less than " + (MIN_TIME_BEFORE_RELOGIN/1000) + " seconds"+
+          " before.");
+      return false;
+    }
+    // register most recent relogin attempt
+    user.setLastLogin(now);
+    return true;
+  }
+  
+  /**
+   * Did the login happen via keytab
+   * @return true or false
+   */
   public synchronized static boolean isLoginKeytabBased() {
-    return keytabFile != null;
+    return loginUser.isKeytab;
   }
+
   /**
    * Create a user from a login name. It is intended to be used for remote
    * users in RPC, since it won't have any credentials.
@@ -540,7 +688,9 @@ public class UserGroupInformation {
     }
     Subject subject = new Subject();
     subject.getPrincipals().add(new User(user));
-    return new UserGroupInformation(subject);
+    UserGroupInformation result = new UserGroupInformation(subject);
+    result.setAuthenticationMethod(AuthenticationMethod.SIMPLE);
+    return result;
   }
 
   public static enum AuthenticationMethod {
@@ -568,9 +718,12 @@ public class UserGroupInformation {
       throw new IllegalArgumentException("Null real user");
     }
     Subject subject = new Subject();
-    subject.getPrincipals().add(new User(user));
-    subject.getPrincipals().add(new RealUser(realUser));
-    return new UserGroupInformation(subject);
+    Set<Principal> principals = subject.getPrincipals();
+    principals.add(new User(user));
+    principals.add(new RealUser(realUser));
+    UserGroupInformation result =new UserGroupInformation(subject);
+    result.setAuthenticationMethod(AuthenticationMethod.PROXY);
+    return result;
   }
 
   public UserGroupInformation getRealUser() {
@@ -668,10 +821,7 @@ public class UserGroupInformation {
    * @return the user's full principal name.
    */
   public String getUserName() {
-    for (User p: subject.getPrincipals(User.class)) {
-      return p.getName();
-    }
-    return null;
+    return user.getName();
   }
 
   /**
@@ -758,9 +908,7 @@ public class UserGroupInformation {
    */
   public synchronized 
   void setAuthenticationMethod(AuthenticationMethod authMethod) {
-    for (User p : subject.getPrincipals(User.class)) {
-      p.setAuthenticationMethod(authMethod);
-    }
+    user.setAuthenticationMethod(authMethod);
   }
 
   /**
@@ -769,10 +917,7 @@ public class UserGroupInformation {
    * @return AuthenticationMethod in the subject, null if not present.
    */
   public synchronized AuthenticationMethod getAuthenticationMethod() {
-    for (User p: subject.getPrincipals(User.class)) {
-      return p.getAuthenticationMethod();
-    }
-    return null;
+    return user.getAuthenticationMethod();
   }
 
   /**
@@ -869,6 +1014,8 @@ public class UserGroupInformation {
     UserGroupInformation ugi = getCurrentUser();
     ugi.print();
     System.out.println("UGI: " + ugi);
+    System.out.println("Auth method " + ugi.user.getAuthenticationMethod());
+    System.out.println("Keytab " + ugi.isKeytab);
     System.out.println("============================================================");
     
     if (args.length == 2) {
@@ -876,6 +1023,8 @@ public class UserGroupInformation {
       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);
     }
   }
 

Modified: hadoop/common/branches/branch-0.20-security-patches/src/test/org/apache/hadoop/security/TestUserGroupInformation.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/branch-0.20-security-patches/src/test/org/apache/hadoop/security/TestUserGroupInformation.java?rev=1077358&r1=1077357&r2=1077358&view=diff
==============================================================================
--- hadoop/common/branches/branch-0.20-security-patches/src/test/org/apache/hadoop/security/TestUserGroupInformation.java (original)
+++ hadoop/common/branches/branch-0.20-security-patches/src/test/org/apache/hadoop/security/TestUserGroupInformation.java Fri Mar  4 04:06:53 2011
@@ -273,8 +273,8 @@ public class TestUserGroupInformation {
     final AuthenticationMethod am = AuthenticationMethod.KERBEROS;
     ugi.setAuthenticationMethod(am);
     Assert.assertEquals(am, ugi.getAuthenticationMethod());
-    Assert.assertEquals(null, proxyUgi.getAuthenticationMethod());
-    proxyUgi.setAuthenticationMethod(AuthenticationMethod.PROXY);
+    Assert.assertEquals(AuthenticationMethod.PROXY, 
+                        proxyUgi.getAuthenticationMethod());
     proxyUgi.doAs(new PrivilegedExceptionAction<Object>() {
       public Object run() throws IOException {
         Assert.assertEquals(AuthenticationMethod.PROXY, UserGroupInformation