001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.security.token.delegation.web;
019    
020    import com.google.common.base.Preconditions;
021    import org.apache.hadoop.classification.InterfaceAudience;
022    import org.apache.hadoop.classification.InterfaceStability;
023    import org.apache.hadoop.io.Text;
024    import org.apache.hadoop.security.Credentials;
025    import org.apache.hadoop.security.SecurityUtil;
026    import org.apache.hadoop.security.UserGroupInformation;
027    import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
028    import org.apache.hadoop.security.authentication.client.AuthenticationException;
029    import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
030    import org.apache.hadoop.security.token.TokenIdentifier;
031    import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
032    
033    import java.io.IOException;
034    import java.net.HttpURLConnection;
035    import java.net.InetSocketAddress;
036    import java.net.URL;
037    import java.net.URLEncoder;
038    import java.util.HashMap;
039    import java.util.Map;
040    
041    /**
042     * The <code>DelegationTokenAuthenticatedURL</code> is a
043     * {@link AuthenticatedURL} sub-class with built-in Hadoop Delegation Token
044     * functionality.
045     * <p/>
046     * The authentication mechanisms supported by default are Hadoop Simple
047     * authentication (also known as pseudo authentication) and Kerberos SPNEGO
048     * authentication.
049     * <p/>
050     * Additional authentication mechanisms can be supported via {@link
051     * DelegationTokenAuthenticator} implementations.
052     * <p/>
053     * The default {@link DelegationTokenAuthenticator} is the {@link
054     * KerberosDelegationTokenAuthenticator} class which supports
055     * automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication via
056     * the {@link PseudoDelegationTokenAuthenticator} class.
057     * <p/>
058     * <code>AuthenticatedURL</code> instances are not thread-safe.
059     */
060    @InterfaceAudience.Public
061    @InterfaceStability.Unstable
062    public class DelegationTokenAuthenticatedURL extends AuthenticatedURL {
063    
064      /**
065       * Constant used in URL's query string to perform a proxy user request, the
066       * value of the <code>DO_AS</code> parameter is the user the request will be
067       * done on behalf of.
068       */
069      static final String DO_AS = "doAs";
070    
071      /**
072       * Client side authentication token that handles Delegation Tokens.
073       */
074      @InterfaceAudience.Public
075      @InterfaceStability.Unstable
076      public static class Token extends AuthenticatedURL.Token {
077        private
078        org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
079            delegationToken;
080    
081        public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
082        getDelegationToken() {
083          return delegationToken;
084        }
085        public void setDelegationToken(
086            org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier> delegationToken) {
087          this.delegationToken = delegationToken;
088        }
089    
090      }
091    
092      private static Class<? extends DelegationTokenAuthenticator>
093          DEFAULT_AUTHENTICATOR = KerberosDelegationTokenAuthenticator.class;
094    
095      /**
096       * Sets the default {@link DelegationTokenAuthenticator} class to use when an
097       * {@link DelegationTokenAuthenticatedURL} instance is created without
098       * specifying one.
099       *
100       * The default class is {@link KerberosDelegationTokenAuthenticator}
101       *
102       * @param authenticator the authenticator class to use as default.
103       */
104      public static void setDefaultDelegationTokenAuthenticator(
105          Class<? extends DelegationTokenAuthenticator> authenticator) {
106        DEFAULT_AUTHENTICATOR = authenticator;
107      }
108    
109      /**
110       * Returns the default {@link DelegationTokenAuthenticator} class to use when
111       * an {@link DelegationTokenAuthenticatedURL} instance is created without
112       * specifying one.
113       * <p/>
114       * The default class is {@link KerberosDelegationTokenAuthenticator}
115       *
116       * @return the delegation token authenticator class to use as default.
117       */
118      public static Class<? extends DelegationTokenAuthenticator>
119          getDefaultDelegationTokenAuthenticator() {
120        return DEFAULT_AUTHENTICATOR;
121      }
122    
123      private static DelegationTokenAuthenticator
124          obtainDelegationTokenAuthenticator(DelegationTokenAuthenticator dta,
125                ConnectionConfigurator connConfigurator) {
126        try {
127          if (dta == null) {
128            dta = DEFAULT_AUTHENTICATOR.newInstance();
129            dta.setConnectionConfigurator(connConfigurator);
130          }
131          return dta;
132        } catch (Exception ex) {
133          throw new IllegalArgumentException(ex);
134        }
135      }
136    
137      private boolean useQueryStringforDelegationToken = false;
138    
139      /**
140       * Creates an <code>DelegationTokenAuthenticatedURL</code>.
141       * <p/>
142       * An instance of the default {@link DelegationTokenAuthenticator} will be
143       * used.
144       */
145      public DelegationTokenAuthenticatedURL() {
146        this(null, null);
147      }
148    
149      /**
150       * Creates an <code>DelegationTokenAuthenticatedURL</code>.
151       *
152       * @param authenticator the {@link DelegationTokenAuthenticator} instance to
153       * use, if <code>null</code> the default one will be used.
154       */
155      public DelegationTokenAuthenticatedURL(
156          DelegationTokenAuthenticator authenticator) {
157        this(authenticator, null);
158      }
159    
160      /**
161       * Creates an <code>DelegationTokenAuthenticatedURL</code> using the default
162       * {@link DelegationTokenAuthenticator} class.
163       *
164       * @param connConfigurator a connection configurator.
165       */
166      public DelegationTokenAuthenticatedURL(
167          ConnectionConfigurator connConfigurator) {
168        this(null, connConfigurator);
169      }
170    
171      /**
172       * Creates an <code>DelegationTokenAuthenticatedURL</code>.
173       *
174       * @param authenticator the {@link DelegationTokenAuthenticator} instance to
175       * use, if <code>null</code> the default one will be used.
176       * @param connConfigurator a connection configurator.
177       */
178      public DelegationTokenAuthenticatedURL(
179          DelegationTokenAuthenticator authenticator,
180          ConnectionConfigurator connConfigurator) {
181        super(obtainDelegationTokenAuthenticator(authenticator, connConfigurator),
182                connConfigurator);
183      }
184    
185      /**
186       * Sets if delegation token should be transmitted in the URL query string.
187       * By default it is transmitted using the
188       * {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP header.
189       * <p/>
190       * This method is provided to enable WebHDFS backwards compatibility.
191       *
192       * @param useQueryString  <code>TRUE</code> if the token is transmitted in the
193       * URL query string, <code>FALSE</code> if the delegation token is transmitted
194       * using the {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP
195       * header.
196       */
197      @Deprecated
198      protected void setUseQueryStringForDelegationToken(boolean useQueryString) {
199        useQueryStringforDelegationToken = useQueryString;
200      }
201    
202      /**
203       * Returns if delegation token is transmitted as a HTTP header.
204       *
205       * @return <code>TRUE</code> if the token is transmitted in the URL query
206       * string, <code>FALSE</code> if the delegation token is transmitted using the
207       * {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP header.
208       */
209      public boolean useQueryStringForDelegationToken() {
210        return useQueryStringforDelegationToken;
211      }
212    
213      /**
214       * Returns an authenticated {@link HttpURLConnection}, it uses a Delegation
215       * Token only if the given auth token is an instance of {@link Token} and
216       * it contains a Delegation Token, otherwise use the configured
217       * {@link DelegationTokenAuthenticator} to authenticate the connection.
218       *
219       * @param url the URL to connect to. Only HTTP/S URLs are supported.
220       * @param token the authentication token being used for the user.
221       * @return an authenticated {@link HttpURLConnection}.
222       * @throws IOException if an IO error occurred.
223       * @throws AuthenticationException if an authentication exception occurred.
224       */
225      @Override
226      public HttpURLConnection openConnection(URL url, AuthenticatedURL.Token token)
227          throws IOException, AuthenticationException {
228        return (token instanceof Token) ? openConnection(url, (Token) token)
229                                        : super.openConnection(url ,token);
230      }
231    
232      /**
233       * Returns an authenticated {@link HttpURLConnection}. If the Delegation
234       * Token is present, it will be used taking precedence over the configured
235       * <code>Authenticator</code>.
236       *
237       * @param url the URL to connect to. Only HTTP/S URLs are supported.
238       * @param token the authentication token being used for the user.
239       * @return an authenticated {@link HttpURLConnection}.
240       * @throws IOException if an IO error occurred.
241       * @throws AuthenticationException if an authentication exception occurred.
242       */
243      public HttpURLConnection openConnection(URL url, Token token)
244          throws IOException, AuthenticationException {
245        return openConnection(url, token, null);
246      }
247    
248      private URL augmentURL(URL url, Map<String, String> params)
249          throws IOException {
250        if (params != null && params.size() > 0) {
251          String urlStr = url.toExternalForm();
252          StringBuilder sb = new StringBuilder(urlStr);
253          String separator = (urlStr.contains("?")) ? "&" : "?";
254          for (Map.Entry<String, String> param : params.entrySet()) {
255            sb.append(separator).append(param.getKey()).append("=").append(
256                param.getValue());
257            separator = "&";
258          }
259          url = new URL(sb.toString());
260        }
261        return url;
262      }
263    
264      /**
265       * Returns an authenticated {@link HttpURLConnection}. If the Delegation
266       * Token is present, it will be used taking precedence over the configured
267       * <code>Authenticator</code>. If the <code>doAs</code> parameter is not NULL,
268       * the request will be done on behalf of the specified <code>doAs</code> user.
269       *
270       * @param url the URL to connect to. Only HTTP/S URLs are supported.
271       * @param token the authentication token being used for the user.
272       * @param doAs user to do the the request on behalf of, if NULL the request is
273       * as self.
274       * @return an authenticated {@link HttpURLConnection}.
275       * @throws IOException if an IO error occurred.
276       * @throws AuthenticationException if an authentication exception occurred.
277       */
278      @SuppressWarnings("unchecked")
279      public HttpURLConnection openConnection(URL url, Token token, String doAs)
280          throws IOException, AuthenticationException {
281        Preconditions.checkNotNull(url, "url");
282        Preconditions.checkNotNull(token, "token");
283        Map<String, String> extraParams = new HashMap<String, String>();
284        org.apache.hadoop.security.token.Token<? extends TokenIdentifier> dToken
285            = null;
286        // if we have valid auth token, it takes precedence over a delegation token
287        // and we don't even look for one.
288        if (!token.isSet()) {
289          // delegation token
290          Credentials creds = UserGroupInformation.getCurrentUser().
291              getCredentials();
292          if (!creds.getAllTokens().isEmpty()) {
293            InetSocketAddress serviceAddr = new InetSocketAddress(url.getHost(),
294                url.getPort());
295            Text service = SecurityUtil.buildTokenService(serviceAddr);
296            dToken = creds.getToken(service);
297            if (dToken != null) {
298              if (useQueryStringForDelegationToken()) {
299                // delegation token will go in the query string, injecting it
300                extraParams.put(
301                    KerberosDelegationTokenAuthenticator.DELEGATION_PARAM,
302                    dToken.encodeToUrlString());
303              } else {
304                // delegation token will go as request header, setting it in the
305                // auth-token to ensure no authentication handshake is triggered
306                // (if we have a delegation token, we are authenticated)
307                // the delegation token header is injected in the connection request
308                // at the end of this method.
309                token.delegationToken = (org.apache.hadoop.security.token.Token
310                    <AbstractDelegationTokenIdentifier>) dToken;
311              }
312            }
313          }
314        }
315    
316        // proxyuser
317        if (doAs != null) {
318          extraParams.put(DO_AS, URLEncoder.encode(doAs, "UTF-8"));
319        }
320    
321        url = augmentURL(url, extraParams);
322        HttpURLConnection conn = super.openConnection(url, token);
323        if (!token.isSet() && !useQueryStringForDelegationToken() && dToken != null) {
324          // injecting the delegation token header in the connection request
325          conn.setRequestProperty(
326              DelegationTokenAuthenticator.DELEGATION_TOKEN_HEADER,
327              dToken.encodeToUrlString());
328        }
329        return conn;
330      }
331    
332      /**
333       * Requests a delegation token using the configured <code>Authenticator</code>
334       * for authentication.
335       *
336       * @param url the URL to get the delegation token from. Only HTTP/S URLs are
337       * supported.
338       * @param token the authentication token being used for the user where the
339       * Delegation token will be stored.
340       * @param renewer the renewer user.
341       * @return a delegation token.
342       * @throws IOException if an IO error occurred.
343       * @throws AuthenticationException if an authentication exception occurred.
344       */
345      public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
346          getDelegationToken(URL url, Token token, String renewer)
347              throws IOException, AuthenticationException {
348        return getDelegationToken(url, token, renewer, null);
349      }
350    
351      /**
352       * Requests a delegation token using the configured <code>Authenticator</code>
353       * for authentication.
354       *
355       * @param url the URL to get the delegation token from. Only HTTP/S URLs are
356       * supported.
357       * @param token the authentication token being used for the user where the
358       * Delegation token will be stored.
359       * @param renewer the renewer user.
360       * @param doAsUser the user to do as, which will be the token owner.
361       * @return a delegation token.
362       * @throws IOException if an IO error occurred.
363       * @throws AuthenticationException if an authentication exception occurred.
364       */
365      public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
366          getDelegationToken(URL url, Token token, String renewer, String doAsUser)
367              throws IOException, AuthenticationException {
368        Preconditions.checkNotNull(url, "url");
369        Preconditions.checkNotNull(token, "token");
370        try {
371          token.delegationToken =
372              ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
373                  getDelegationToken(url, token, renewer, doAsUser);
374          return token.delegationToken;
375        } catch (IOException ex) {
376          token.delegationToken = null;
377          throw ex;
378        }
379      }
380    
381      /**
382       * Renews a delegation token from the server end-point using the
383       * configured <code>Authenticator</code> for authentication.
384       *
385       * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
386       * supported.
387       * @param token the authentication token with the Delegation Token to renew.
388       * @throws IOException if an IO error occurred.
389       * @throws AuthenticationException if an authentication exception occurred.
390       */
391      public long renewDelegationToken(URL url, Token token)
392          throws IOException, AuthenticationException {
393        return renewDelegationToken(url, token, null);
394      }
395    
396      /**
397       * Renews a delegation token from the server end-point using the
398       * configured <code>Authenticator</code> for authentication.
399       *
400       * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
401       * supported.
402       * @param token the authentication token with the Delegation Token to renew.
403       * @param doAsUser the user to do as, which will be the token owner.
404       * @throws IOException if an IO error occurred.
405       * @throws AuthenticationException if an authentication exception occurred.
406       */
407      public long renewDelegationToken(URL url, Token token, String doAsUser)
408          throws IOException, AuthenticationException {
409        Preconditions.checkNotNull(url, "url");
410        Preconditions.checkNotNull(token, "token");
411        Preconditions.checkNotNull(token.delegationToken,
412            "No delegation token available");
413        try {
414          return ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
415              renewDelegationToken(url, token, token.delegationToken, doAsUser);
416        } catch (IOException ex) {
417          token.delegationToken = null;
418          throw ex;
419        }
420      }
421    
422      /**
423       * Cancels a delegation token from the server end-point. It does not require
424       * being authenticated by the configured <code>Authenticator</code>.
425       *
426       * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
427       * are supported.
428       * @param token the authentication token with the Delegation Token to cancel.
429       * @throws IOException if an IO error occurred.
430       */
431      public void cancelDelegationToken(URL url, Token token)
432          throws IOException {
433        cancelDelegationToken(url, token, null);
434      }
435    
436      /**
437       * Cancels a delegation token from the server end-point. It does not require
438       * being authenticated by the configured <code>Authenticator</code>.
439       *
440       * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
441       * are supported.
442       * @param token the authentication token with the Delegation Token to cancel.
443       * @param doAsUser the user to do as, which will be the token owner.
444       * @throws IOException if an IO error occurred.
445       */
446      public void cancelDelegationToken(URL url, Token token, String doAsUser)
447          throws IOException {
448        Preconditions.checkNotNull(url, "url");
449        Preconditions.checkNotNull(token, "token");
450        Preconditions.checkNotNull(token.delegationToken,
451            "No delegation token available");
452        try {
453          ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
454              cancelDelegationToken(url, token, token.delegationToken, doAsUser);
455        } finally {
456          token.delegationToken = null;
457        }
458      }
459    
460    }