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 }