001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with this
004 * work for additional information regarding copyright ownership. The ASF
005 * licenses this file to you under the Apache License, Version 2.0 (the
006 * "License"); you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 * 
009 * http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations under
015 * the License.
016 */
017package org.apache.hadoop.security;
018
019import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION;
020
021import java.io.IOException;
022import java.net.InetAddress;
023import java.net.InetSocketAddress;
024import java.net.URI;
025import java.net.URL;
026import java.net.URLConnection;
027import java.net.UnknownHostException;
028import java.security.AccessController;
029import java.security.PrivilegedAction;
030import java.security.PrivilegedExceptionAction;
031import java.util.Arrays;
032import java.util.List;
033import java.util.Locale;
034import java.util.ServiceLoader;
035import java.util.Set;
036
037import javax.security.auth.Subject;
038import javax.security.auth.kerberos.KerberosPrincipal;
039import javax.security.auth.kerberos.KerberosTicket;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.apache.hadoop.classification.InterfaceAudience;
044import org.apache.hadoop.classification.InterfaceStability;
045import org.apache.hadoop.conf.Configuration;
046import org.apache.hadoop.fs.CommonConfigurationKeys;
047import org.apache.hadoop.http.HttpConfig;
048import org.apache.hadoop.io.Text;
049import org.apache.hadoop.net.NetUtils;
050import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
051import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
052import org.apache.hadoop.security.authentication.client.AuthenticationException;
053import org.apache.hadoop.security.ssl.SSLFactory;
054import org.apache.hadoop.security.token.Token;
055import org.apache.hadoop.security.token.TokenInfo;
056
057import com.google.common.annotations.VisibleForTesting;
058
059//this will need to be replaced someday when there is a suitable replacement
060import sun.net.dns.ResolverConfiguration;
061import sun.net.util.IPAddressUtil;
062
063@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
064@InterfaceStability.Evolving
065public class SecurityUtil {
066  public static final Log LOG = LogFactory.getLog(SecurityUtil.class);
067  public static final String HOSTNAME_PATTERN = "_HOST";
068
069  // controls whether buildTokenService will use an ip or host/ip as given
070  // by the user
071  @VisibleForTesting
072  static boolean useIpForTokenService;
073  @VisibleForTesting
074  static HostResolver hostResolver;
075
076  private static SSLFactory sslFactory;
077
078  static {
079    Configuration conf = new Configuration();
080    boolean useIp = conf.getBoolean(
081      CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP,
082      CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT);
083    setTokenServiceUseIp(useIp);
084    if (HttpConfig.isSecure()) {
085      sslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, conf);
086      try {
087        sslFactory.init();
088      } catch (Exception ex) {
089        throw new RuntimeException(ex);
090      }
091    }
092  }
093  
094  /**
095   * For use only by tests and initialization
096   */
097  @InterfaceAudience.Private
098  static void setTokenServiceUseIp(boolean flag) {
099    useIpForTokenService = flag;
100    hostResolver = !useIpForTokenService
101        ? new QualifiedHostResolver()
102        : new StandardHostResolver();
103  }
104  
105  /**
106   * Find the original TGT within the current subject's credentials. Cross-realm
107   * TGT's of the form "krbtgt/[email protected]" may be present.
108   * 
109   * @return The TGT from the current subject
110   * @throws IOException
111   *           if TGT can't be found
112   */
113  private static KerberosTicket getTgtFromSubject() throws IOException {
114    Subject current = Subject.getSubject(AccessController.getContext());
115    if (current == null) {
116      throw new IOException(
117          "Can't get TGT from current Subject, because it is null");
118    }
119    Set<KerberosTicket> tickets = current
120        .getPrivateCredentials(KerberosTicket.class);
121    for (KerberosTicket t : tickets) {
122      if (isOriginalTGT(t))
123        return t;
124    }
125    throw new IOException("Failed to find TGT from current Subject:"+current);
126  }
127  
128  /**
129   * TGS must have the server principal of the form "krbtgt/FOO@FOO".
130   * @param principal
131   * @return true or false
132   */
133  static boolean 
134  isTGSPrincipal(KerberosPrincipal principal) {
135    if (principal == null)
136      return false;
137    if (principal.getName().equals("krbtgt/" + principal.getRealm() + 
138        "@" + principal.getRealm())) {
139      return true;
140    }
141    return false;
142  }
143  
144  /**
145   * Check whether the server principal is the TGS's principal
146   * @param ticket the original TGT (the ticket that is obtained when a 
147   * kinit is done)
148   * @return true or false
149   */
150  protected static boolean isOriginalTGT(KerberosTicket ticket) {
151    return isTGSPrincipal(ticket.getServer());
152  }
153
154  /**
155   * Convert Kerberos principal name pattern to valid Kerberos principal
156   * names. It replaces hostname pattern with hostname, which should be
157   * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses
158   * dynamically looked-up fqdn of the current host instead.
159   * 
160   * @param principalConfig
161   *          the Kerberos principal name conf value to convert
162   * @param hostname
163   *          the fully-qualified domain name used for substitution
164   * @return converted Kerberos principal name
165   * @throws IOException if the client address cannot be determined
166   */
167  @InterfaceAudience.Public
168  @InterfaceStability.Evolving
169  public static String getServerPrincipal(String principalConfig,
170      String hostname) throws IOException {
171    String[] components = getComponents(principalConfig);
172    if (components == null || components.length != 3
173        || !components[1].equals(HOSTNAME_PATTERN)) {
174      return principalConfig;
175    } else {
176      return replacePattern(components, hostname);
177    }
178  }
179  
180  /**
181   * Convert Kerberos principal name pattern to valid Kerberos principal names.
182   * This method is similar to {@link #getServerPrincipal(String, String)},
183   * except 1) the reverse DNS lookup from addr to hostname is done only when
184   * necessary, 2) param addr can't be null (no default behavior of using local
185   * hostname when addr is null).
186   * 
187   * @param principalConfig
188   *          Kerberos principal name pattern to convert
189   * @param addr
190   *          InetAddress of the host used for substitution
191   * @return converted Kerberos principal name
192   * @throws IOException if the client address cannot be determined
193   */
194  @InterfaceAudience.Public
195  @InterfaceStability.Evolving
196  public static String getServerPrincipal(String principalConfig,
197      InetAddress addr) throws IOException {
198    String[] components = getComponents(principalConfig);
199    if (components == null || components.length != 3
200        || !components[1].equals(HOSTNAME_PATTERN)) {
201      return principalConfig;
202    } else {
203      if (addr == null) {
204        throw new IOException("Can't replace " + HOSTNAME_PATTERN
205            + " pattern since client address is null");
206      }
207      return replacePattern(components, addr.getCanonicalHostName());
208    }
209  }
210  
211  private static String[] getComponents(String principalConfig) {
212    if (principalConfig == null)
213      return null;
214    return principalConfig.split("[/@]");
215  }
216  
217  private static String replacePattern(String[] components, String hostname)
218      throws IOException {
219    String fqdn = hostname;
220    if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) {
221      fqdn = getLocalHostName();
222    }
223    return components[0] + "/" + fqdn.toLowerCase(Locale.US) + "@" + components[2];
224  }
225  
226  static String getLocalHostName() throws UnknownHostException {
227    return InetAddress.getLocalHost().getCanonicalHostName();
228  }
229
230  /**
231   * Login as a principal specified in config. Substitute $host in
232   * user's Kerberos principal name with a dynamically looked-up fully-qualified
233   * domain name of the current host.
234   * 
235   * @param conf
236   *          conf to use
237   * @param keytabFileKey
238   *          the key to look for keytab file in conf
239   * @param userNameKey
240   *          the key to look for user's Kerberos principal name in conf
241   * @throws IOException if login fails
242   */
243  @InterfaceAudience.Public
244  @InterfaceStability.Evolving
245  public static void login(final Configuration conf,
246      final String keytabFileKey, final String userNameKey) throws IOException {
247    login(conf, keytabFileKey, userNameKey, getLocalHostName());
248  }
249
250  /**
251   * Login as a principal specified in config. Substitute $host in user's Kerberos principal 
252   * name with hostname. If non-secure mode - return. If no keytab available -
253   * bail out with an exception
254   * 
255   * @param conf
256   *          conf to use
257   * @param keytabFileKey
258   *          the key to look for keytab file in conf
259   * @param userNameKey
260   *          the key to look for user's Kerberos principal name in conf
261   * @param hostname
262   *          hostname to use for substitution
263   * @throws IOException if the config doesn't specify a keytab
264   */
265  @InterfaceAudience.Public
266  @InterfaceStability.Evolving
267  public static void login(final Configuration conf,
268      final String keytabFileKey, final String userNameKey, String hostname)
269      throws IOException {
270    
271    if(! UserGroupInformation.isSecurityEnabled()) 
272      return;
273    
274    String keytabFilename = conf.get(keytabFileKey);
275    if (keytabFilename == null || keytabFilename.length() == 0) {
276      throw new IOException("Running in secure mode, but config doesn't have a keytab");
277    }
278
279    String principalConfig = conf.get(userNameKey, System
280        .getProperty("user.name"));
281    String principalName = SecurityUtil.getServerPrincipal(principalConfig,
282        hostname);
283    UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename);
284  }
285
286  /**
287   * create the service name for a Delegation token
288   * @param uri of the service
289   * @param defPort is used if the uri lacks a port
290   * @return the token service, or null if no authority
291   * @see #buildTokenService(InetSocketAddress)
292   */
293  public static String buildDTServiceName(URI uri, int defPort) {
294    String authority = uri.getAuthority();
295    if (authority == null) {
296      return null;
297    }
298    InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort);
299    return buildTokenService(addr).toString();
300   }
301  
302  /**
303   * Get the host name from the principal name of format <service>/host@realm.
304   * @param principalName principal name of format as described above
305   * @return host name if the the string conforms to the above format, else null
306   */
307  public static String getHostFromPrincipal(String principalName) {
308    return new HadoopKerberosName(principalName).getHostName();
309  }
310
311  private static ServiceLoader<SecurityInfo> securityInfoProviders = 
312    ServiceLoader.load(SecurityInfo.class);
313  private static SecurityInfo[] testProviders = new SecurityInfo[0];
314
315  /**
316   * Test setup method to register additional providers.
317   * @param providers a list of high priority providers to use
318   */
319  @InterfaceAudience.Private
320  public static void setSecurityInfoProviders(SecurityInfo... providers) {
321    testProviders = providers;
322  }
323  
324  /**
325   * Look up the KerberosInfo for a given protocol. It searches all known
326   * SecurityInfo providers.
327   * @param protocol the protocol class to get the information for
328   * @param conf configuration object
329   * @return the KerberosInfo or null if it has no KerberosInfo defined
330   */
331  public static KerberosInfo 
332  getKerberosInfo(Class<?> protocol, Configuration conf) {
333    synchronized (testProviders) {
334      for(SecurityInfo provider: testProviders) {
335        KerberosInfo result = provider.getKerberosInfo(protocol, conf);
336        if (result != null) {
337          return result;
338        }
339      }
340    }
341    
342    synchronized (securityInfoProviders) {
343      for(SecurityInfo provider: securityInfoProviders) {
344        KerberosInfo result = provider.getKerberosInfo(protocol, conf);
345        if (result != null) {
346          return result;
347        }
348      }
349    }
350    return null;
351  }
352 
353  /**
354   * Look up the TokenInfo for a given protocol. It searches all known
355   * SecurityInfo providers.
356   * @param protocol The protocol class to get the information for.
357   * @param conf Configuration object
358   * @return the TokenInfo or null if it has no KerberosInfo defined
359   */
360  public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) {
361    synchronized (testProviders) {
362      for(SecurityInfo provider: testProviders) {
363        TokenInfo result = provider.getTokenInfo(protocol, conf);
364        if (result != null) {
365          return result;
366        }      
367      }
368    }
369    
370    synchronized (securityInfoProviders) {
371      for(SecurityInfo provider: securityInfoProviders) {
372        TokenInfo result = provider.getTokenInfo(protocol, conf);
373        if (result != null) {
374          return result;
375        }
376      } 
377    }
378    
379    return null;
380  }
381
382  /**
383   * Decode the given token's service field into an InetAddress
384   * @param token from which to obtain the service
385   * @return InetAddress for the service
386   */
387  public static InetSocketAddress getTokenServiceAddr(Token<?> token) {
388    return NetUtils.createSocketAddr(token.getService().toString());
389  }
390
391  /**
392   * Set the given token's service to the format expected by the RPC client 
393   * @param token a delegation token
394   * @param addr the socket for the rpc connection
395   */
396  public static void setTokenService(Token<?> token, InetSocketAddress addr) {
397    Text service = buildTokenService(addr);
398    if (token != null) {
399      token.setService(service);
400      if (LOG.isDebugEnabled()) {
401        LOG.debug("Acquired token "+token);  // Token#toString() prints service
402      }
403    } else {
404      LOG.warn("Failed to get token for service "+service);
405    }
406  }
407  
408  /**
409   * Construct the service key for a token
410   * @param addr InetSocketAddress of remote connection with a token
411   * @return "ip:port" or "host:port" depending on the value of
412   *          hadoop.security.token.service.use_ip
413   */
414  public static Text buildTokenService(InetSocketAddress addr) {
415    String host = null;
416    if (useIpForTokenService) {
417      if (addr.isUnresolved()) { // host has no ip address
418        throw new IllegalArgumentException(
419            new UnknownHostException(addr.getHostName())
420        );
421      }
422      host = addr.getAddress().getHostAddress();
423    } else {
424      host = addr.getHostName().toLowerCase();
425    }
426    return new Text(host + ":" + addr.getPort());
427  }
428
429  /**
430   * Construct the service key for a token
431   * @param uri of remote connection with a token
432   * @return "ip:port" or "host:port" depending on the value of
433   *          hadoop.security.token.service.use_ip
434   */
435  public static Text buildTokenService(URI uri) {
436    return buildTokenService(NetUtils.createSocketAddr(uri.getAuthority()));
437  }
438  
439  /**
440   * Perform the given action as the daemon's login user. If the login
441   * user cannot be determined, this will log a FATAL error and exit
442   * the whole JVM.
443   */
444  public static <T> T doAsLoginUserOrFatal(PrivilegedAction<T> action) { 
445    if (UserGroupInformation.isSecurityEnabled()) {
446      UserGroupInformation ugi = null;
447      try { 
448        ugi = UserGroupInformation.getLoginUser();
449      } catch (IOException e) {
450        LOG.fatal("Exception while getting login user", e);
451        e.printStackTrace();
452        Runtime.getRuntime().exit(-1);
453      }
454      return ugi.doAs(action);
455    } else {
456      return action.run();
457    }
458  }
459  
460  /**
461   * Perform the given action as the daemon's login user. If an
462   * InterruptedException is thrown, it is converted to an IOException.
463   *
464   * @param action the action to perform
465   * @return the result of the action
466   * @throws IOException in the event of error
467   */
468  public static <T> T doAsLoginUser(PrivilegedExceptionAction<T> action)
469      throws IOException {
470    return doAsUser(UserGroupInformation.getLoginUser(), action);
471  }
472
473  /**
474   * Perform the given action as the daemon's current user. If an
475   * InterruptedException is thrown, it is converted to an IOException.
476   *
477   * @param action the action to perform
478   * @return the result of the action
479   * @throws IOException in the event of error
480   */
481  public static <T> T doAsCurrentUser(PrivilegedExceptionAction<T> action)
482      throws IOException {
483    return doAsUser(UserGroupInformation.getCurrentUser(), action);
484  }
485
486  private static <T> T doAsUser(UserGroupInformation ugi,
487      PrivilegedExceptionAction<T> action) throws IOException {
488    try {
489      return ugi.doAs(action);
490    } catch (InterruptedException ie) {
491      throw new IOException(ie);
492    }
493  }
494
495  /**
496   * Open a (if need be) secure connection to a URL in a secure environment
497   * that is using SPNEGO to authenticate its URLs. All Namenode and Secondary
498   * Namenode URLs that are protected via SPNEGO should be accessed via this
499   * method.
500   *
501   * @param url to authenticate via SPNEGO.
502   * @return A connection that has been authenticated via SPNEGO
503   * @throws IOException If unable to authenticate via SPNEGO
504   */
505  public static URLConnection openSecureHttpConnection(URL url) throws IOException {
506    if (!HttpConfig.isSecure() && !UserGroupInformation.isSecurityEnabled()) {
507      return url.openConnection();
508    }
509
510    AuthenticatedURL.Token token = new AuthenticatedURL.Token();
511    try {
512      return new AuthenticatedURL(null, sslFactory).openConnection(url, token);
513    } catch (AuthenticationException e) {
514      throw new IOException("Exception trying to open authenticated connection to "
515              + url, e);
516    }
517  }
518
519  /**
520   * Resolves a host subject to the security requirements determined by
521   * hadoop.security.token.service.use_ip.
522   * 
523   * @param hostname host or ip to resolve
524   * @return a resolved host
525   * @throws UnknownHostException if the host doesn't exist
526   */
527  @InterfaceAudience.Private
528  public static
529  InetAddress getByName(String hostname) throws UnknownHostException {
530    return hostResolver.getByName(hostname);
531  }
532  
533  interface HostResolver {
534    InetAddress getByName(String host) throws UnknownHostException;    
535  }
536  
537  /**
538   * Uses standard java host resolution
539   */
540  static class StandardHostResolver implements HostResolver {
541    @Override
542    public InetAddress getByName(String host) throws UnknownHostException {
543      return InetAddress.getByName(host);
544    }
545  }
546  
547  /**
548   * This an alternate resolver with important properties that the standard
549   * java resolver lacks:
550   * 1) The hostname is fully qualified.  This avoids security issues if not
551   *    all hosts in the cluster do not share the same search domains.  It
552   *    also prevents other hosts from performing unnecessary dns searches.
553   *    In contrast, InetAddress simply returns the host as given.
554   * 2) The InetAddress is instantiated with an exact host and IP to prevent
555   *    further unnecessary lookups.  InetAddress may perform an unnecessary
556   *    reverse lookup for an IP.
557   * 3) A call to getHostName() will always return the qualified hostname, or
558   *    more importantly, the IP if instantiated with an IP.  This avoids
559   *    unnecessary dns timeouts if the host is not resolvable.
560   * 4) Point 3 also ensures that if the host is re-resolved, ex. during a
561   *    connection re-attempt, that a reverse lookup to host and forward
562   *    lookup to IP is not performed since the reverse/forward mappings may
563   *    not always return the same IP.  If the client initiated a connection
564   *    with an IP, then that IP is all that should ever be contacted.
565   *    
566   * NOTE: this resolver is only used if:
567   *       hadoop.security.token.service.use_ip=false 
568   */
569  protected static class QualifiedHostResolver implements HostResolver {
570    @SuppressWarnings("unchecked")
571    private List<String> searchDomains =
572        ResolverConfiguration.open().searchlist();
573    
574    /**
575     * Create an InetAddress with a fully qualified hostname of the given
576     * hostname.  InetAddress does not qualify an incomplete hostname that
577     * is resolved via the domain search list.
578     * {@link InetAddress#getCanonicalHostName()} will fully qualify the
579     * hostname, but it always return the A record whereas the given hostname
580     * may be a CNAME.
581     * 
582     * @param host a hostname or ip address
583     * @return InetAddress with the fully qualified hostname or ip
584     * @throws UnknownHostException if host does not exist
585     */
586    @Override
587    public InetAddress getByName(String host) throws UnknownHostException {
588      InetAddress addr = null;
589
590      if (IPAddressUtil.isIPv4LiteralAddress(host)) {
591        // use ipv4 address as-is
592        byte[] ip = IPAddressUtil.textToNumericFormatV4(host);
593        addr = InetAddress.getByAddress(host, ip);
594      } else if (IPAddressUtil.isIPv6LiteralAddress(host)) {
595        // use ipv6 address as-is
596        byte[] ip = IPAddressUtil.textToNumericFormatV6(host);
597        addr = InetAddress.getByAddress(host, ip);
598      } else if (host.endsWith(".")) {
599        // a rooted host ends with a dot, ex. "host."
600        // rooted hosts never use the search path, so only try an exact lookup
601        addr = getByExactName(host);
602      } else if (host.contains(".")) {
603        // the host contains a dot (domain), ex. "host.domain"
604        // try an exact host lookup, then fallback to search list
605        addr = getByExactName(host);
606        if (addr == null) {
607          addr = getByNameWithSearch(host);
608        }
609      } else {
610        // it's a simple host with no dots, ex. "host"
611        // try the search list, then fallback to exact host
612        InetAddress loopback = InetAddress.getByName(null);
613        if (host.equalsIgnoreCase(loopback.getHostName())) {
614          addr = InetAddress.getByAddress(host, loopback.getAddress());
615        } else {
616          addr = getByNameWithSearch(host);
617          if (addr == null) {
618            addr = getByExactName(host);
619          }
620        }
621      }
622      // unresolvable!
623      if (addr == null) {
624        throw new UnknownHostException(host);
625      }
626      return addr;
627    }
628
629    InetAddress getByExactName(String host) {
630      InetAddress addr = null;
631      // InetAddress will use the search list unless the host is rooted
632      // with a trailing dot.  The trailing dot will disable any use of the
633      // search path in a lower level resolver.  See RFC 1535.
634      String fqHost = host;
635      if (!fqHost.endsWith(".")) fqHost += ".";
636      try {
637        addr = getInetAddressByName(fqHost);
638        // can't leave the hostname as rooted or other parts of the system
639        // malfunction, ex. kerberos principals are lacking proper host
640        // equivalence for rooted/non-rooted hostnames
641        addr = InetAddress.getByAddress(host, addr.getAddress());
642      } catch (UnknownHostException e) {
643        // ignore, caller will throw if necessary
644      }
645      return addr;
646    }
647
648    InetAddress getByNameWithSearch(String host) {
649      InetAddress addr = null;
650      if (host.endsWith(".")) { // already qualified?
651        addr = getByExactName(host); 
652      } else {
653        for (String domain : searchDomains) {
654          String dot = !domain.startsWith(".") ? "." : "";
655          addr = getByExactName(host + dot + domain);
656          if (addr != null) break;
657        }
658      }
659      return addr;
660    }
661
662    // implemented as a separate method to facilitate unit testing
663    InetAddress getInetAddressByName(String host) throws UnknownHostException {
664      return InetAddress.getByName(host);
665    }
666
667    void setSearchDomains(String ... domains) {
668      searchDomains = Arrays.asList(domains);
669    }
670  }
671
672  public static AuthenticationMethod getAuthenticationMethod(Configuration conf) {
673    String value = conf.get(HADOOP_SECURITY_AUTHENTICATION, "simple");
674    try {
675      return Enum.valueOf(AuthenticationMethod.class, value.toUpperCase());
676    } catch (IllegalArgumentException iae) {
677      throw new IllegalArgumentException("Invalid attribute value for " +
678          HADOOP_SECURITY_AUTHENTICATION + " of " + value);
679    }
680  }
681
682  public static void setAuthenticationMethod(
683      AuthenticationMethod authenticationMethod, Configuration conf) {
684    if (authenticationMethod == null) {
685      authenticationMethod = AuthenticationMethod.SIMPLE;
686    }
687    conf.set(HADOOP_SECURITY_AUTHENTICATION,
688             authenticationMethod.toString().toLowerCase());
689  }
690}