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