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