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     */
017    package org.apache.hadoop.security;
018    
019    import java.io.IOException;
020    import java.lang.reflect.Constructor;
021    import java.lang.reflect.Field;
022    import java.lang.reflect.InvocationTargetException;
023    import java.lang.reflect.Method;
024    import java.net.InetAddress;
025    import java.net.InetSocketAddress;
026    import java.net.URI;
027    import java.net.URL;
028    import java.net.UnknownHostException;
029    import java.security.AccessController;
030    import java.util.Arrays;
031    import java.util.List;
032    import java.util.ServiceLoader;
033    import java.util.Set;
034    
035    import javax.security.auth.Subject;
036    import javax.security.auth.kerberos.KerberosPrincipal;
037    import javax.security.auth.kerberos.KerberosTicket;
038    
039    import org.apache.commons.logging.Log;
040    import org.apache.commons.logging.LogFactory;
041    import org.apache.hadoop.classification.InterfaceAudience;
042    import org.apache.hadoop.classification.InterfaceStability;
043    import org.apache.hadoop.conf.Configuration;
044    import org.apache.hadoop.fs.CommonConfigurationKeys;
045    import org.apache.hadoop.io.Text;
046    import org.apache.hadoop.net.NetUtils;
047    import org.apache.hadoop.security.token.Token;
048    import org.apache.hadoop.security.token.TokenInfo;
049    
050    import com.google.common.annotations.VisibleForTesting;
051    
052    //this will need to be replaced someday when there is a suitable replacement
053    import sun.net.dns.ResolverConfiguration;
054    import sun.net.util.IPAddressUtil;
055    
056    @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
057    @InterfaceStability.Evolving
058    public 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    }