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