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