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