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 }