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