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