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