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