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