001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.security;
019    
020    import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION;
021    
022    import java.io.IOException;
023    import java.lang.reflect.UndeclaredThrowableException;
024    import java.security.AccessControlContext;
025    import java.security.AccessController;
026    import java.security.Principal;
027    import java.security.PrivilegedAction;
028    import java.security.PrivilegedActionException;
029    import java.security.PrivilegedExceptionAction;
030    import java.util.ArrayList;
031    import java.util.Arrays;
032    import java.util.Collection;
033    import java.util.Collections;
034    import java.util.HashMap;
035    import java.util.List;
036    import java.util.Map;
037    import java.util.Set;
038    
039    import javax.security.auth.Subject;
040    import javax.security.auth.callback.CallbackHandler;
041    import javax.security.auth.kerberos.KerberosKey;
042    import javax.security.auth.kerberos.KerberosPrincipal;
043    import javax.security.auth.kerberos.KerberosTicket;
044    import javax.security.auth.login.AppConfigurationEntry;
045    import javax.security.auth.login.LoginContext;
046    import javax.security.auth.login.LoginException;
047    import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
048    import javax.security.auth.spi.LoginModule;
049    
050    import org.apache.commons.logging.Log;
051    import org.apache.commons.logging.LogFactory;
052    import org.apache.hadoop.classification.InterfaceAudience;
053    import org.apache.hadoop.classification.InterfaceStability;
054    import org.apache.hadoop.conf.Configuration;
055    import org.apache.hadoop.fs.Path;
056    import org.apache.hadoop.metrics2.annotation.Metric;
057    import org.apache.hadoop.metrics2.annotation.Metrics;
058    import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
059    import org.apache.hadoop.metrics2.lib.MutableRate;
060    import org.apache.hadoop.security.authentication.util.KerberosName;
061    import org.apache.hadoop.security.authentication.util.KerberosUtil;
062    import org.apache.hadoop.security.token.Token;
063    import org.apache.hadoop.security.token.TokenIdentifier;
064    import org.apache.hadoop.util.Shell;
065    
066    /**
067     * User and group information for Hadoop.
068     * This class wraps around a JAAS Subject and provides methods to determine the
069     * user's username and groups. It supports both the Windows, Unix and Kerberos 
070     * login modules.
071     */
072    @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce", "HBase", "Hive", "Oozie"})
073    @InterfaceStability.Evolving
074    public class UserGroupInformation {
075      private static final Log LOG =  LogFactory.getLog(UserGroupInformation.class);
076      /**
077       * Percentage of the ticket window to use before we renew ticket.
078       */
079      private static final float TICKET_RENEW_WINDOW = 0.80f;
080      static final String HADOOP_USER_NAME = "HADOOP_USER_NAME";
081      
082      /** 
083       * UgiMetrics maintains UGI activity statistics
084       * and publishes them through the metrics interfaces.
085       */
086      @Metrics(about="User and group related metrics", context="ugi")
087      static class UgiMetrics {
088        @Metric("Rate of successful kerberos logins and latency (milliseconds)")
089        MutableRate loginSuccess;
090        @Metric("Rate of failed kerberos logins and latency (milliseconds)")
091        MutableRate loginFailure;
092    
093        static UgiMetrics create() {
094          return DefaultMetricsSystem.instance().register(new UgiMetrics());
095        }
096      }
097      
098      /**
099       * A login module that looks at the Kerberos, Unix, or Windows principal and
100       * adds the corresponding UserName.
101       */
102      @InterfaceAudience.Private
103      public static class HadoopLoginModule implements LoginModule {
104        private Subject subject;
105    
106        @Override
107        public boolean abort() throws LoginException {
108          return true;
109        }
110    
111        private <T extends Principal> T getCanonicalUser(Class<T> cls) {
112          for(T user: subject.getPrincipals(cls)) {
113            return user;
114          }
115          return null;
116        }
117    
118        @Override
119        public boolean commit() throws LoginException {
120          if (LOG.isDebugEnabled()) {
121            LOG.debug("hadoop login commit");
122          }
123          // if we already have a user, we are done.
124          if (!subject.getPrincipals(User.class).isEmpty()) {
125            if (LOG.isDebugEnabled()) {
126              LOG.debug("using existing subject:"+subject.getPrincipals());
127            }
128            return true;
129          }
130          Principal user = null;
131          // if we are using kerberos, try it out
132          if (useKerberos) {
133            user = getCanonicalUser(KerberosPrincipal.class);
134            if (LOG.isDebugEnabled()) {
135              LOG.debug("using kerberos user:"+user);
136            }
137          }
138          //If we don't have a kerberos user and security is disabled, check
139          //if user is specified in the environment or properties
140          if (!isSecurityEnabled() && (user == null)) {
141            String envUser = System.getenv(HADOOP_USER_NAME);
142            if (envUser == null) {
143              envUser = System.getProperty(HADOOP_USER_NAME);
144            }
145            user = envUser == null ? null : new User(envUser);
146          }
147          // use the OS user
148          if (user == null) {
149            user = getCanonicalUser(OS_PRINCIPAL_CLASS);
150            if (LOG.isDebugEnabled()) {
151              LOG.debug("using local user:"+user);
152            }
153          }
154          // if we found the user, add our principal
155          if (user != null) {
156            subject.getPrincipals().add(new User(user.getName()));
157            return true;
158          }
159          LOG.error("Can't find user in " + subject);
160          throw new LoginException("Can't find user name");
161        }
162    
163        @Override
164        public void initialize(Subject subject, CallbackHandler callbackHandler,
165                               Map<String, ?> sharedState, Map<String, ?> options) {
166          this.subject = subject;
167        }
168    
169        @Override
170        public boolean login() throws LoginException {
171          if (LOG.isDebugEnabled()) {
172            LOG.debug("hadoop login");
173          }
174          return true;
175        }
176    
177        @Override
178        public boolean logout() throws LoginException {
179          if (LOG.isDebugEnabled()) {
180            LOG.debug("hadoop logout");
181          }
182          return true;
183        }
184      }
185    
186      /** Metrics to track UGI activity */
187      static UgiMetrics metrics = UgiMetrics.create();
188      /** Are the static variables that depend on configuration initialized? */
189      private static boolean isInitialized = false;
190      /** Should we use Kerberos configuration? */
191      private static boolean useKerberos;
192      /** Server-side groups fetching service */
193      private static Groups groups;
194      /** The configuration to use */
195      private static Configuration conf;
196    
197      
198      /** Leave 10 minutes between relogin attempts. */
199      private static final long MIN_TIME_BEFORE_RELOGIN = 10 * 60 * 1000L;
200      
201      /**Environment variable pointing to the token cache file*/
202      public static final String HADOOP_TOKEN_FILE_LOCATION = 
203        "HADOOP_TOKEN_FILE_LOCATION";
204      
205      /** 
206       * A method to initialize the fields that depend on a configuration.
207       * Must be called before useKerberos or groups is used.
208       */
209      private static synchronized void ensureInitialized() {
210        if (!isInitialized) {
211            initialize(new Configuration(), KerberosName.hasRulesBeenSet());
212        }
213      }
214    
215      /**
216       * Initialize UGI and related classes.
217       * @param conf the configuration to use
218       */
219      private static synchronized void initialize(Configuration conf, boolean skipRulesSetting) {
220        initUGI(conf);
221        // give the configuration on how to translate Kerberos names
222        try {
223          if (!skipRulesSetting) {
224            HadoopKerberosName.setConfiguration(conf);
225          }
226        } catch (IOException ioe) {
227          throw new RuntimeException("Problem with Kerberos auth_to_local name " +
228              "configuration", ioe);
229        }
230      }
231      
232      /**
233       * Set the configuration values for UGI.
234       * @param conf the configuration to use
235       */
236      private static synchronized void initUGI(Configuration conf) {
237        String value = conf.get(HADOOP_SECURITY_AUTHENTICATION);
238        if (value == null || "simple".equals(value)) {
239          useKerberos = false;
240        } else if ("kerberos".equals(value)) {
241          useKerberos = true;
242        } else {
243          throw new IllegalArgumentException("Invalid attribute value for " +
244                                             HADOOP_SECURITY_AUTHENTICATION + 
245                                             " of " + value);
246        }
247        // If we haven't set up testing groups, use the configuration to find it
248        if (!(groups instanceof TestingGroups)) {
249          groups = Groups.getUserToGroupsMappingService(conf);
250        }
251        isInitialized = true;
252        UserGroupInformation.conf = conf;
253      }
254    
255      /**
256       * Set the static configuration for UGI.
257       * In particular, set the security authentication mechanism and the
258       * group look up service.
259       * @param conf the configuration to use
260       */
261      @InterfaceAudience.Public
262      @InterfaceStability.Evolving
263      public static void setConfiguration(Configuration conf) {
264        initialize(conf, false);
265      }
266      
267      /**
268       * Determine if UserGroupInformation is using Kerberos to determine
269       * user identities or is relying on simple authentication
270       * 
271       * @return true if UGI is working in a secure environment
272       */
273      public static boolean isSecurityEnabled() {
274        ensureInitialized();
275        return useKerberos;
276      }
277      
278      /**
279       * Information about the logged in user.
280       */
281      private static UserGroupInformation loginUser = null;
282      private static String keytabPrincipal = null;
283      private static String keytabFile = null;
284    
285      private final Subject subject;
286      // All non-static fields must be read-only caches that come from the subject.
287      private final User user;
288      private final boolean isKeytab;
289      private final boolean isKrbTkt;
290      
291      private static String OS_LOGIN_MODULE_NAME;
292      private static Class<? extends Principal> OS_PRINCIPAL_CLASS;
293      private static final boolean windows = 
294                               System.getProperty("os.name").startsWith("Windows");
295      /* Return the OS login module class name */
296      private static String getOSLoginModuleName() {
297        if (System.getProperty("java.vendor").contains("IBM")) {
298          return windows ? "com.ibm.security.auth.module.NTLoginModule"
299           : "com.ibm.security.auth.module.LinuxLoginModule";
300        } else {
301          return windows ? "com.sun.security.auth.module.NTLoginModule"
302            : "com.sun.security.auth.module.UnixLoginModule";
303        }
304      }
305    
306      /* Return the OS principal class */
307      @SuppressWarnings("unchecked")
308      private static Class<? extends Principal> getOsPrincipalClass() {
309        ClassLoader cl = ClassLoader.getSystemClassLoader();
310        try {
311          if (System.getProperty("java.vendor").contains("IBM")) {
312            if (windows) {
313              return (Class<? extends Principal>)
314                cl.loadClass("com.ibm.security.auth.UsernamePrincipal");
315            } else {
316              return (Class<? extends Principal>)
317                (System.getProperty("os.arch").contains("64")
318                 ? cl.loadClass("com.ibm.security.auth.UsernamePrincipal")
319                 : cl.loadClass("com.ibm.security.auth.LinuxPrincipal"));
320            }
321          } else {
322            return (Class<? extends Principal>) (windows
323               ? cl.loadClass("com.sun.security.auth.NTUserPrincipal")
324               : cl.loadClass("com.sun.security.auth.UnixPrincipal"));
325          }
326        } catch (ClassNotFoundException e) {
327          LOG.error("Unable to find JAAS classes:" + e.getMessage());
328        }
329        return null;
330      }
331      static {
332        OS_LOGIN_MODULE_NAME = getOSLoginModuleName();
333        OS_PRINCIPAL_CLASS = getOsPrincipalClass();
334      }
335    
336      private static class RealUser implements Principal {
337        private final UserGroupInformation realUser;
338        
339        RealUser(UserGroupInformation realUser) {
340          this.realUser = realUser;
341        }
342        
343        public String getName() {
344          return realUser.getUserName();
345        }
346        
347        public UserGroupInformation getRealUser() {
348          return realUser;
349        }
350        
351        @Override
352        public boolean equals(Object o) {
353          if (this == o) {
354            return true;
355          } else if (o == null || getClass() != o.getClass()) {
356            return false;
357          } else {
358            return realUser.equals(((RealUser) o).realUser);
359          }
360        }
361        
362        @Override
363        public int hashCode() {
364          return realUser.hashCode();
365        }
366        
367        @Override
368        public String toString() {
369          return realUser.toString();
370        }
371      }
372      
373      /**
374       * A JAAS configuration that defines the login modules that we want
375       * to use for login.
376       */
377      private static class HadoopConfiguration 
378          extends javax.security.auth.login.Configuration {
379        private static final String SIMPLE_CONFIG_NAME = "hadoop-simple";
380        private static final String USER_KERBEROS_CONFIG_NAME = 
381          "hadoop-user-kerberos";
382        private static final String KEYTAB_KERBEROS_CONFIG_NAME = 
383          "hadoop-keytab-kerberos";
384    
385        private static final Map<String, String> BASIC_JAAS_OPTIONS =
386          new HashMap<String,String>();
387        static {
388          String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG");
389          if (jaasEnvVar != null && "true".equalsIgnoreCase(jaasEnvVar)) {
390            BASIC_JAAS_OPTIONS.put("debug", "true");
391          }
392        }
393        
394        private static final AppConfigurationEntry OS_SPECIFIC_LOGIN =
395          new AppConfigurationEntry(OS_LOGIN_MODULE_NAME,
396                                    LoginModuleControlFlag.REQUIRED,
397                                    BASIC_JAAS_OPTIONS);
398        private static final AppConfigurationEntry HADOOP_LOGIN =
399          new AppConfigurationEntry(HadoopLoginModule.class.getName(),
400                                    LoginModuleControlFlag.REQUIRED,
401                                    BASIC_JAAS_OPTIONS);
402        private static final Map<String,String> USER_KERBEROS_OPTIONS = 
403          new HashMap<String,String>();
404        static {
405          USER_KERBEROS_OPTIONS.put("doNotPrompt", "true");
406          USER_KERBEROS_OPTIONS.put("useTicketCache", "true");
407          USER_KERBEROS_OPTIONS.put("renewTGT", "true");
408          String ticketCache = System.getenv("KRB5CCNAME");
409          if (ticketCache != null) {
410            USER_KERBEROS_OPTIONS.put("ticketCache", ticketCache);
411          }
412          USER_KERBEROS_OPTIONS.putAll(BASIC_JAAS_OPTIONS);
413        }
414        private static final AppConfigurationEntry USER_KERBEROS_LOGIN =
415          new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(),
416                                    LoginModuleControlFlag.OPTIONAL,
417                                    USER_KERBEROS_OPTIONS);
418        private static final Map<String,String> KEYTAB_KERBEROS_OPTIONS = 
419          new HashMap<String,String>();
420        static {
421          KEYTAB_KERBEROS_OPTIONS.put("doNotPrompt", "true");
422          KEYTAB_KERBEROS_OPTIONS.put("useKeyTab", "true");
423          KEYTAB_KERBEROS_OPTIONS.put("storeKey", "true");
424          KEYTAB_KERBEROS_OPTIONS.put("refreshKrb5Config", "true");
425          KEYTAB_KERBEROS_OPTIONS.putAll(BASIC_JAAS_OPTIONS);      
426        }
427        private static final AppConfigurationEntry KEYTAB_KERBEROS_LOGIN =
428          new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(),
429                                    LoginModuleControlFlag.REQUIRED,
430                                    KEYTAB_KERBEROS_OPTIONS);
431        
432        private static final AppConfigurationEntry[] SIMPLE_CONF = 
433          new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, HADOOP_LOGIN};
434    
435        private static final AppConfigurationEntry[] USER_KERBEROS_CONF =
436          new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, USER_KERBEROS_LOGIN,
437                                      HADOOP_LOGIN};
438    
439        private static final AppConfigurationEntry[] KEYTAB_KERBEROS_CONF =
440          new AppConfigurationEntry[]{KEYTAB_KERBEROS_LOGIN, HADOOP_LOGIN};
441    
442        @Override
443        public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
444          if (SIMPLE_CONFIG_NAME.equals(appName)) {
445            return SIMPLE_CONF;
446          } else if (USER_KERBEROS_CONFIG_NAME.equals(appName)) {
447            return USER_KERBEROS_CONF;
448          } else if (KEYTAB_KERBEROS_CONFIG_NAME.equals(appName)) {
449            KEYTAB_KERBEROS_OPTIONS.put("keyTab", keytabFile);
450            KEYTAB_KERBEROS_OPTIONS.put("principal", keytabPrincipal);
451            return KEYTAB_KERBEROS_CONF;
452          }
453          return null;
454        }
455      }
456      
457      private static LoginContext
458      newLoginContext(String appName, Subject subject) throws LoginException {
459        // Temporarily switch the thread's ContextClassLoader to match this
460        // class's classloader, so that we can properly load HadoopLoginModule
461        // from the JAAS libraries.
462        Thread t = Thread.currentThread();
463        ClassLoader oldCCL = t.getContextClassLoader();
464        t.setContextClassLoader(HadoopLoginModule.class.getClassLoader());
465        try {
466          return new LoginContext(appName, subject, null, new HadoopConfiguration());
467        } finally {
468          t.setContextClassLoader(oldCCL);
469        }
470      }
471    
472      private LoginContext getLogin() {
473        return user.getLogin();
474      }
475      
476      private void setLogin(LoginContext login) {
477        user.setLogin(login);
478      }
479    
480      /**
481       * Create a UserGroupInformation for the given subject.
482       * This does not change the subject or acquire new credentials.
483       * @param subject the user's subject
484       */
485      UserGroupInformation(Subject subject) {
486        this.subject = subject;
487        this.user = subject.getPrincipals(User.class).iterator().next();
488        this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
489        this.isKrbTkt = !subject.getPrivateCredentials(KerberosTicket.class).isEmpty();
490      }
491      
492      /**
493       * checks if logged in using kerberos
494       * @return true if the subject logged via keytab or has a Kerberos TGT
495       */
496      public boolean hasKerberosCredentials() {
497        return isKeytab || isKrbTkt;
498      }
499    
500      /**
501       * Return the current user, including any doAs in the current stack.
502       * @return the current user
503       * @throws IOException if login fails
504       */
505      @InterfaceAudience.Public
506      @InterfaceStability.Evolving
507      public synchronized
508      static UserGroupInformation getCurrentUser() throws IOException {
509        AccessControlContext context = AccessController.getContext();
510        Subject subject = Subject.getSubject(context);
511        if (subject == null || subject.getPrincipals(User.class).isEmpty()) {
512          return getLoginUser();
513        } else {
514          return new UserGroupInformation(subject);
515        }
516      }
517    
518      /**
519       * Get the currently logged in user.
520       * @return the logged in user
521       * @throws IOException if login fails
522       */
523      @InterfaceAudience.Public
524      @InterfaceStability.Evolving
525      public synchronized 
526      static UserGroupInformation getLoginUser() throws IOException {
527        if (loginUser == null) {
528          try {
529            Subject subject = new Subject();
530            LoginContext login;
531            if (isSecurityEnabled()) {
532              login = newLoginContext(HadoopConfiguration.USER_KERBEROS_CONFIG_NAME,
533                  subject);
534            } else {
535              login = newLoginContext(HadoopConfiguration.SIMPLE_CONFIG_NAME, 
536                  subject);
537            }
538            login.login();
539            loginUser = new UserGroupInformation(subject);
540            loginUser.setLogin(login);
541            loginUser.setAuthenticationMethod(isSecurityEnabled() ?
542                                              AuthenticationMethod.KERBEROS :
543                                              AuthenticationMethod.SIMPLE);
544            loginUser = new UserGroupInformation(login.getSubject());
545            String fileLocation = System.getenv(HADOOP_TOKEN_FILE_LOCATION);
546            if (fileLocation != null && isSecurityEnabled()) {
547              // load the token storage file and put all of the tokens into the
548              // user.
549              Credentials cred = Credentials.readTokenStorageFile(
550                  new Path("file:///" + fileLocation), conf);
551              for (Token<?> token: cred.getAllTokens()) {
552                loginUser.addToken(token);
553              }
554            }
555            loginUser.spawnAutoRenewalThreadForUserCreds();
556          } catch (LoginException le) {
557            throw new IOException("failure to login", le);
558          }
559          if (LOG.isDebugEnabled()) {
560            LOG.debug("UGI loginUser:"+loginUser);
561          }
562        }
563        return loginUser;
564      }
565    
566      /**
567       * Is this user logged in from a keytab file?
568       * @return true if the credentials are from a keytab file.
569       */
570      public boolean isFromKeytab() {
571        return isKeytab;
572      }
573      
574      /**
575       * Get the Kerberos TGT
576       * @return the user's TGT or null if none was found
577       */
578      private synchronized KerberosTicket getTGT() {
579        Set<KerberosTicket> tickets = subject
580            .getPrivateCredentials(KerberosTicket.class);
581        for (KerberosTicket ticket : tickets) {
582          if (SecurityUtil.isOriginalTGT(ticket)) {
583            if (LOG.isDebugEnabled()) {
584              LOG.debug("Found tgt " + ticket);
585            }
586            return ticket;
587          }
588        }
589        return null;
590      }
591      
592      private long getRefreshTime(KerberosTicket tgt) {
593        long start = tgt.getStartTime().getTime();
594        long end = tgt.getEndTime().getTime();
595        return start + (long) ((end - start) * TICKET_RENEW_WINDOW);
596      }
597    
598      /**Spawn a thread to do periodic renewals of kerberos credentials*/
599      private void spawnAutoRenewalThreadForUserCreds() {
600        if (isSecurityEnabled()) {
601          //spawn thread only if we have kerb credentials
602          if (user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS &&
603              !isKeytab) {
604            Thread t = new Thread(new Runnable() {
605              
606              public void run() {
607                String cmd = conf.get("hadoop.kerberos.kinit.command",
608                                      "kinit");
609                KerberosTicket tgt = getTGT();
610                if (tgt == null) {
611                  return;
612                }
613                long nextRefresh = getRefreshTime(tgt);
614                while (true) {
615                  try {
616                    long now = System.currentTimeMillis();
617                    if(LOG.isDebugEnabled()) {
618                      LOG.debug("Current time is " + now);
619                      LOG.debug("Next refresh is " + nextRefresh);
620                    }
621                    if (now < nextRefresh) {
622                      Thread.sleep(nextRefresh - now);
623                    }
624                    Shell.execCommand(cmd, "-R");
625                    if(LOG.isDebugEnabled()) {
626                      LOG.debug("renewed ticket");
627                    }
628                    reloginFromTicketCache();
629                    tgt = getTGT();
630                    if (tgt == null) {
631                      LOG.warn("No TGT after renewal. Aborting renew thread for " +
632                               getUserName());
633                      return;
634                    }
635                    nextRefresh = Math.max(getRefreshTime(tgt),
636                                           now + MIN_TIME_BEFORE_RELOGIN);
637                  } catch (InterruptedException ie) {
638                    LOG.warn("Terminating renewal thread");
639                    return;
640                  } catch (IOException ie) {
641                    LOG.warn("Exception encountered while running the" +
642                        " renewal command. Aborting renew thread. " + ie);
643                    return;
644                  }
645                }
646              }
647            });
648            t.setDaemon(true);
649            t.setName("TGT Renewer for " + getUserName());
650            t.start();
651          }
652        }
653      }
654      /**
655       * Log a user in from a keytab file. Loads a user identity from a keytab
656       * file and logs them in. They become the currently logged-in user.
657       * @param user the principal name to load from the keytab
658       * @param path the path to the keytab file
659       * @throws IOException if the keytab file can't be read
660       */
661      @InterfaceAudience.Public
662      @InterfaceStability.Evolving
663      public synchronized
664      static void loginUserFromKeytab(String user,
665                                      String path
666                                      ) throws IOException {
667        if (!isSecurityEnabled())
668          return;
669    
670        keytabFile = path;
671        keytabPrincipal = user;
672        Subject subject = new Subject();
673        LoginContext login; 
674        long start = 0;
675        try {
676          login = 
677            newLoginContext(HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, subject);
678          start = System.currentTimeMillis();
679          login.login();
680          metrics.loginSuccess.add(System.currentTimeMillis() - start);
681          loginUser = new UserGroupInformation(subject);
682          loginUser.setLogin(login);
683          loginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS);
684        } catch (LoginException le) {
685          if (start > 0) {
686            metrics.loginFailure.add(System.currentTimeMillis() - start);
687          }
688          throw new IOException("Login failure for " + user + " from keytab " + 
689                                path, le);
690        }
691        LOG.info("Login successful for user " + keytabPrincipal
692            + " using keytab file " + keytabFile);
693      }
694      
695      /**
696       * Re-login a user from keytab if TGT is expired or is close to expiry.
697       * 
698       * @throws IOException
699       */
700      public synchronized void checkTGTAndReloginFromKeytab() throws IOException {
701        if (!isSecurityEnabled()
702            || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS
703            || !isKeytab)
704          return;
705        KerberosTicket tgt = getTGT();
706        if (tgt != null && System.currentTimeMillis() < getRefreshTime(tgt)) {
707          return;
708        }
709        reloginFromKeytab();
710      }
711    
712      /**
713       * Re-Login a user in from a keytab file. Loads a user identity from a keytab
714       * file and logs them in. They become the currently logged-in user. This
715       * method assumes that {@link #loginUserFromKeytab(String, String)} had 
716       * happened already.
717       * The Subject field of this UserGroupInformation object is updated to have
718       * the new credentials.
719       * @throws IOException on a failure
720       */
721      @InterfaceAudience.Public
722      @InterfaceStability.Evolving
723      public synchronized void reloginFromKeytab()
724      throws IOException {
725        if (!isSecurityEnabled() ||
726             user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
727             !isKeytab)
728          return;
729        
730        long now = System.currentTimeMillis();
731        if (!hasSufficientTimeElapsed(now)) {
732          return;
733        }
734    
735        KerberosTicket tgt = getTGT();
736        //Return if TGT is valid and is not going to expire soon.
737        if (tgt != null && now < getRefreshTime(tgt)) {
738          return;
739        }
740        
741        LoginContext login = getLogin();
742        if (login == null || keytabFile == null) {
743          throw new IOException("loginUserFromKeyTab must be done first");
744        }
745        
746        long start = 0;
747        // register most recent relogin attempt
748        user.setLastLogin(now);
749        try {
750          LOG.info("Initiating logout for " + getUserName());
751          synchronized (UserGroupInformation.class) {
752            // clear up the kerberos state. But the tokens are not cleared! As per
753            // the Java kerberos login module code, only the kerberos credentials
754            // are cleared
755            login.logout();
756            // login and also update the subject field of this instance to
757            // have the new credentials (pass it to the LoginContext constructor)
758            login = newLoginContext(
759                HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, getSubject());
760            LOG.info("Initiating re-login for " + keytabPrincipal);
761            start = System.currentTimeMillis();
762            login.login();
763            metrics.loginSuccess.add(System.currentTimeMillis() - start);
764            setLogin(login);
765          }
766        } catch (LoginException le) {
767          if (start > 0) {
768            metrics.loginFailure.add(System.currentTimeMillis() - start);
769          }
770          throw new IOException("Login failure for " + keytabPrincipal + 
771              " from keytab " + keytabFile, le);
772        } 
773      }
774    
775      /**
776       * Re-Login a user in from the ticket cache.  This
777       * method assumes that login had happened already.
778       * The Subject field of this UserGroupInformation object is updated to have
779       * the new credentials.
780       * @throws IOException on a failure
781       */
782      @InterfaceAudience.Public
783      @InterfaceStability.Evolving
784      public synchronized void reloginFromTicketCache()
785      throws IOException {
786        if (!isSecurityEnabled() || 
787            user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
788            !isKrbTkt)
789          return;
790        LoginContext login = getLogin();
791        if (login == null) {
792          throw new IOException("login must be done first");
793        }
794        long now = System.currentTimeMillis();
795        if (!hasSufficientTimeElapsed(now)) {
796          return;
797        }
798        // register most recent relogin attempt
799        user.setLastLogin(now);
800        try {
801          LOG.info("Initiating logout for " + getUserName());
802          //clear up the kerberos state. But the tokens are not cleared! As per 
803          //the Java kerberos login module code, only the kerberos credentials
804          //are cleared
805          login.logout();
806          //login and also update the subject field of this instance to 
807          //have the new credentials (pass it to the LoginContext constructor)
808          login = 
809            newLoginContext(HadoopConfiguration.USER_KERBEROS_CONFIG_NAME, 
810                getSubject());
811          LOG.info("Initiating re-login for " + getUserName());
812          login.login();
813          setLogin(login);
814        } catch (LoginException le) {
815          throw new IOException("Login failure for " + getUserName(), le);
816        } 
817      }
818    
819    
820      /**
821       * Log a user in from a keytab file. Loads a user identity from a keytab
822       * file and login them in. This new user does not affect the currently
823       * logged-in user.
824       * @param user the principal name to load from the keytab
825       * @param path the path to the keytab file
826       * @throws IOException if the keytab file can't be read
827       */
828      public synchronized
829      static UserGroupInformation loginUserFromKeytabAndReturnUGI(String user,
830                                      String path
831                                      ) throws IOException {
832        if (!isSecurityEnabled())
833          return UserGroupInformation.getCurrentUser();
834        String oldKeytabFile = null;
835        String oldKeytabPrincipal = null;
836    
837        long start = 0;
838        try {
839          oldKeytabFile = keytabFile;
840          oldKeytabPrincipal = keytabPrincipal;
841          keytabFile = path;
842          keytabPrincipal = user;
843          Subject subject = new Subject();
844          
845          LoginContext login = 
846            newLoginContext(HadoopConfiguration.KEYTAB_KERBEROS_CONFIG_NAME, subject); 
847           
848          start = System.currentTimeMillis();
849          login.login();
850          metrics.loginSuccess.add(System.currentTimeMillis() - start);
851          UserGroupInformation newLoginUser = new UserGroupInformation(subject);
852          newLoginUser.setLogin(login);
853          newLoginUser.setAuthenticationMethod(AuthenticationMethod.KERBEROS);
854          
855          return newLoginUser;
856        } catch (LoginException le) {
857          if (start > 0) {
858            metrics.loginFailure.add(System.currentTimeMillis() - start);
859          }
860          throw new IOException("Login failure for " + user + " from keytab " + 
861                                path, le);
862        } finally {
863          if(oldKeytabFile != null) keytabFile = oldKeytabFile;
864          if(oldKeytabPrincipal != null) keytabPrincipal = oldKeytabPrincipal;
865        }
866      }
867    
868      private boolean hasSufficientTimeElapsed(long now) {
869        if (now - user.getLastLogin() < MIN_TIME_BEFORE_RELOGIN ) {
870          LOG.warn("Not attempting to re-login since the last re-login was " +
871              "attempted less than " + (MIN_TIME_BEFORE_RELOGIN/1000) + " seconds"+
872              " before.");
873          return false;
874        }
875        return true;
876      }
877      
878      /**
879       * Did the login happen via keytab
880       * @return true or false
881       */
882      @InterfaceAudience.Public
883      @InterfaceStability.Evolving
884      public synchronized static boolean isLoginKeytabBased() throws IOException {
885        return getLoginUser().isKeytab;
886      }
887    
888      /**
889       * Create a user from a login name. It is intended to be used for remote
890       * users in RPC, since it won't have any credentials.
891       * @param user the full user principal name, must not be empty or null
892       * @return the UserGroupInformation for the remote user.
893       */
894      @InterfaceAudience.Public
895      @InterfaceStability.Evolving
896      public static UserGroupInformation createRemoteUser(String user) {
897        if (user == null || "".equals(user)) {
898          throw new IllegalArgumentException("Null user");
899        }
900        Subject subject = new Subject();
901        subject.getPrincipals().add(new User(user));
902        UserGroupInformation result = new UserGroupInformation(subject);
903        result.setAuthenticationMethod(AuthenticationMethod.SIMPLE);
904        return result;
905      }
906    
907      /**
908       * existing types of authentications' methods
909       */
910      @InterfaceAudience.Public
911      @InterfaceStability.Evolving
912      public static enum AuthenticationMethod {
913        SIMPLE,
914        KERBEROS,
915        TOKEN,
916        CERTIFICATE,
917        KERBEROS_SSL,
918        PROXY;
919      }
920    
921      /**
922       * Create a proxy user using username of the effective user and the ugi of the
923       * real user.
924       * @param user
925       * @param realUser
926       * @return proxyUser ugi
927       */
928      @InterfaceAudience.Public
929      @InterfaceStability.Evolving
930      public static UserGroupInformation createProxyUser(String user,
931          UserGroupInformation realUser) {
932        if (user == null || "".equals(user)) {
933          throw new IllegalArgumentException("Null user");
934        }
935        if (realUser == null) {
936          throw new IllegalArgumentException("Null real user");
937        }
938        Subject subject = new Subject();
939        Set<Principal> principals = subject.getPrincipals();
940        principals.add(new User(user));
941        principals.add(new RealUser(realUser));
942        UserGroupInformation result =new UserGroupInformation(subject);
943        result.setAuthenticationMethod(AuthenticationMethod.PROXY);
944        return result;
945      }
946    
947      /**
948       * get RealUser (vs. EffectiveUser)
949       * @return realUser running over proxy user
950       */
951      @InterfaceAudience.Public
952      @InterfaceStability.Evolving
953      public UserGroupInformation getRealUser() {
954        for (RealUser p: subject.getPrincipals(RealUser.class)) {
955          return p.getRealUser();
956        }
957        return null;
958      }
959    
960    
961      
962      /**
963       * This class is used for storing the groups for testing. It stores a local
964       * map that has the translation of usernames to groups.
965       */
966      private static class TestingGroups extends Groups {
967        private final Map<String, List<String>> userToGroupsMapping = 
968          new HashMap<String,List<String>>();
969        private Groups underlyingImplementation;
970        
971        private TestingGroups(Groups underlyingImplementation) {
972          super(new org.apache.hadoop.conf.Configuration());
973          this.underlyingImplementation = underlyingImplementation;
974        }
975        
976        @Override
977        public List<String> getGroups(String user) throws IOException {
978          List<String> result = userToGroupsMapping.get(user);
979          
980          if (result == null) {
981            result = underlyingImplementation.getGroups(user);
982          }
983    
984          return result;
985        }
986    
987        private void setUserGroups(String user, String[] groups) {
988          userToGroupsMapping.put(user, Arrays.asList(groups));
989        }
990      }
991    
992      /**
993       * Create a UGI for testing HDFS and MapReduce
994       * @param user the full user principal name
995       * @param userGroups the names of the groups that the user belongs to
996       * @return a fake user for running unit tests
997       */
998      @InterfaceAudience.Public
999      @InterfaceStability.Evolving
1000      public static UserGroupInformation createUserForTesting(String user, 
1001                                                              String[] userGroups) {
1002        ensureInitialized();
1003        UserGroupInformation ugi = createRemoteUser(user);
1004        // make sure that the testing object is setup
1005        if (!(groups instanceof TestingGroups)) {
1006          groups = new TestingGroups(groups);
1007        }
1008        // add the user groups
1009        ((TestingGroups) groups).setUserGroups(ugi.getShortUserName(), userGroups);
1010        return ugi;
1011      }
1012    
1013    
1014      /**
1015       * Create a proxy user UGI for testing HDFS and MapReduce
1016       * 
1017       * @param user
1018       *          the full user principal name for effective user
1019       * @param realUser
1020       *          UGI of the real user
1021       * @param userGroups
1022       *          the names of the groups that the user belongs to
1023       * @return a fake user for running unit tests
1024       */
1025      public static UserGroupInformation createProxyUserForTesting(String user,
1026          UserGroupInformation realUser, String[] userGroups) {
1027        ensureInitialized();
1028        UserGroupInformation ugi = createProxyUser(user, realUser);
1029        // make sure that the testing object is setup
1030        if (!(groups instanceof TestingGroups)) {
1031          groups = new TestingGroups(groups);
1032        }
1033        // add the user groups
1034        ((TestingGroups) groups).setUserGroups(ugi.getShortUserName(), userGroups);
1035        return ugi;
1036      }
1037      
1038      /**
1039       * Get the user's login name.
1040       * @return the user's name up to the first '/' or '@'.
1041       */
1042      public String getShortUserName() {
1043        for (User p: subject.getPrincipals(User.class)) {
1044          return p.getShortName();
1045        }
1046        return null;
1047      }
1048    
1049      /**
1050       * Get the user's full principal name.
1051       * @return the user's full principal name.
1052       */
1053      @InterfaceAudience.Public
1054      @InterfaceStability.Evolving
1055      public String getUserName() {
1056        return user.getName();
1057      }
1058    
1059      /**
1060       * Add a TokenIdentifier to this UGI. The TokenIdentifier has typically been
1061       * authenticated by the RPC layer as belonging to the user represented by this
1062       * UGI.
1063       * 
1064       * @param tokenId
1065       *          tokenIdentifier to be added
1066       * @return true on successful add of new tokenIdentifier
1067       */
1068      public synchronized boolean addTokenIdentifier(TokenIdentifier tokenId) {
1069        return subject.getPublicCredentials().add(tokenId);
1070      }
1071    
1072      /**
1073       * Get the set of TokenIdentifiers belonging to this UGI
1074       * 
1075       * @return the set of TokenIdentifiers belonging to this UGI
1076       */
1077      public synchronized Set<TokenIdentifier> getTokenIdentifiers() {
1078        return subject.getPublicCredentials(TokenIdentifier.class);
1079      }
1080      
1081      /**
1082       * Add a token to this UGI
1083       * 
1084       * @param token Token to be added
1085       * @return true on successful add of new token
1086       */
1087      public synchronized boolean addToken(Token<? extends TokenIdentifier> token) {
1088        return subject.getPrivateCredentials().add(token);
1089      }
1090      
1091      /**
1092       * Obtain the collection of tokens associated with this user.
1093       * 
1094       * @return an unmodifiable collection of tokens associated with user
1095       */
1096      public synchronized
1097      Collection<Token<? extends TokenIdentifier>> getTokens() {
1098        Set<Object> creds = subject.getPrivateCredentials();
1099        List<Token<?>> result = new ArrayList<Token<?>>(creds.size());
1100        for(Object o: creds) {
1101          if (o instanceof Token<?>) {
1102            result.add((Token<?>) o);
1103          }
1104        }
1105        return Collections.unmodifiableList(result);
1106      }
1107    
1108      /**
1109       * Get the group names for this user.
1110       * @return the list of users with the primary group first. If the command
1111       *    fails, it returns an empty list.
1112       */
1113      public synchronized String[] getGroupNames() {
1114        ensureInitialized();
1115        try {
1116          List<String> result = groups.getGroups(getShortUserName());
1117          return result.toArray(new String[result.size()]);
1118        } catch (IOException ie) {
1119          LOG.warn("No groups available for user " + getShortUserName());
1120          return new String[0];
1121        }
1122      }
1123      
1124      /**
1125       * Return the username.
1126       */
1127      @Override
1128      public String toString() {
1129        StringBuilder sb = new StringBuilder(getUserName());
1130        sb.append(" (auth:"+getAuthenticationMethod()+")");
1131        if (getRealUser() != null) {
1132          sb.append(" via ").append(getRealUser().toString());
1133        }
1134        return sb.toString();
1135      }
1136    
1137      /**
1138       * Sets the authentication method in the subject
1139       * 
1140       * @param authMethod
1141       */
1142      public synchronized 
1143      void setAuthenticationMethod(AuthenticationMethod authMethod) {
1144        user.setAuthenticationMethod(authMethod);
1145      }
1146    
1147      /**
1148       * Get the authentication method from the subject
1149       * 
1150       * @return AuthenticationMethod in the subject, null if not present.
1151       */
1152      public synchronized AuthenticationMethod getAuthenticationMethod() {
1153        return user.getAuthenticationMethod();
1154      }
1155      
1156      /**
1157       * Returns the authentication method of a ugi. If the authentication method is
1158       * PROXY, returns the authentication method of the real user.
1159       * 
1160       * @param ugi
1161       * @return AuthenticationMethod
1162       */
1163      public static AuthenticationMethod getRealAuthenticationMethod(
1164          UserGroupInformation ugi) {
1165        AuthenticationMethod authMethod = ugi.getAuthenticationMethod();
1166        if (authMethod == AuthenticationMethod.PROXY) {
1167          authMethod = ugi.getRealUser().getAuthenticationMethod();
1168        }
1169        return authMethod;
1170      }
1171    
1172      /**
1173       * Compare the subjects to see if they are equal to each other.
1174       */
1175      @Override
1176      public boolean equals(Object o) {
1177        if (o == this) {
1178          return true;
1179        } else if (o == null || getClass() != o.getClass()) {
1180          return false;
1181        } else {
1182          return subject == ((UserGroupInformation) o).subject;
1183        }
1184      }
1185    
1186      /**
1187       * Return the hash of the subject.
1188       */
1189      @Override
1190      public int hashCode() {
1191        return System.identityHashCode(subject);
1192      }
1193    
1194      /**
1195       * Get the underlying subject from this ugi.
1196       * @return the subject that represents this user.
1197       */
1198      protected Subject getSubject() {
1199        return subject;
1200      }
1201    
1202      /**
1203       * Run the given action as the user.
1204       * @param <T> the return type of the run method
1205       * @param action the method to execute
1206       * @return the value from the run method
1207       */
1208      @InterfaceAudience.Public
1209      @InterfaceStability.Evolving
1210      public <T> T doAs(PrivilegedAction<T> action) {
1211        logPrivilegedAction(subject, action);
1212        return Subject.doAs(subject, action);
1213      }
1214      
1215      /**
1216       * Run the given action as the user, potentially throwing an exception.
1217       * @param <T> the return type of the run method
1218       * @param action the method to execute
1219       * @return the value from the run method
1220       * @throws IOException if the action throws an IOException
1221       * @throws Error if the action throws an Error
1222       * @throws RuntimeException if the action throws a RuntimeException
1223       * @throws InterruptedException if the action throws an InterruptedException
1224       * @throws UndeclaredThrowableException if the action throws something else
1225       */
1226      @InterfaceAudience.Public
1227      @InterfaceStability.Evolving
1228      public <T> T doAs(PrivilegedExceptionAction<T> action
1229                        ) throws IOException, InterruptedException {
1230        try {
1231          logPrivilegedAction(subject, action);
1232          return Subject.doAs(subject, action);
1233        } catch (PrivilegedActionException pae) {
1234          Throwable cause = pae.getCause();
1235          LOG.error("PriviledgedActionException as:"+this+" cause:"+cause);
1236          if (cause instanceof IOException) {
1237            throw (IOException) cause;
1238          } else if (cause instanceof Error) {
1239            throw (Error) cause;
1240          } else if (cause instanceof RuntimeException) {
1241            throw (RuntimeException) cause;
1242          } else if (cause instanceof InterruptedException) {
1243            throw (InterruptedException) cause;
1244          } else {
1245            throw new UndeclaredThrowableException(pae,"Unknown exception in doAs");
1246          }
1247        }
1248      }
1249    
1250      private void logPrivilegedAction(Subject subject, Object action) {
1251        if (LOG.isDebugEnabled()) {
1252          // would be nice if action included a descriptive toString()
1253          String where = new Throwable().getStackTrace()[2].toString();
1254          LOG.debug("PrivilegedAction as:"+this+" from:"+where);
1255        }
1256      }
1257    
1258      private void print() throws IOException {
1259        System.out.println("User: " + getUserName());
1260        System.out.print("Group Ids: ");
1261        System.out.println();
1262        String[] groups = getGroupNames();
1263        System.out.print("Groups: ");
1264        for(int i=0; i < groups.length; i++) {
1265          System.out.print(groups[i] + " ");
1266        }
1267        System.out.println();    
1268      }
1269    
1270      /**
1271       * A test method to print out the current user's UGI.
1272       * @param args if there are two arguments, read the user from the keytab
1273       * and print it out.
1274       * @throws Exception
1275       */
1276      public static void main(String [] args) throws Exception {
1277      System.out.println("Getting UGI for current user");
1278        UserGroupInformation ugi = getCurrentUser();
1279        ugi.print();
1280        System.out.println("UGI: " + ugi);
1281        System.out.println("Auth method " + ugi.user.getAuthenticationMethod());
1282        System.out.println("Keytab " + ugi.isKeytab);
1283        System.out.println("============================================================");
1284        
1285        if (args.length == 2) {
1286          System.out.println("Getting UGI from keytab....");
1287          loginUserFromKeytab(args[0], args[1]);
1288          getCurrentUser().print();
1289          System.out.println("Keytab: " + ugi);
1290          System.out.println("Auth method " + loginUser.user.getAuthenticationMethod());
1291          System.out.println("Keytab " + loginUser.isKeytab);
1292        }
1293      }
1294    
1295    }