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 org.apache.hadoop.classification.InterfaceAudience;
021import org.apache.hadoop.classification.InterfaceStability;
022import org.apache.hadoop.security.SecurityUtil;
023import org.apache.hadoop.security.UserGroupInformation;
024import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
025import org.apache.hadoop.security.authentication.client.AuthenticationException;
026import org.apache.hadoop.security.authentication.client.Authenticator;
027import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
028import org.apache.hadoop.security.token.Token;
029import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
030import org.apache.hadoop.util.HttpExceptionUtils;
031import org.codehaus.jackson.map.ObjectMapper;
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 * {@link Authenticator} wrapper that enhances an {@link Authenticator} with
045 * Delegation Token support.
046 */
047@InterfaceAudience.Public
048@InterfaceStability.Evolving
049public abstract class DelegationTokenAuthenticator implements Authenticator {
050  private static Logger LOG = 
051      LoggerFactory.getLogger(DelegationTokenAuthenticator.class);
052  
053  private static final String CONTENT_TYPE = "Content-Type";
054  private static final String APPLICATION_JSON_MIME = "application/json";
055
056  private static final String HTTP_GET = "GET";
057  private static final String HTTP_PUT = "PUT";
058
059  public static final String OP_PARAM = "op";
060  private static final String OP_PARAM_EQUALS = OP_PARAM + "=";
061
062  public static final String DELEGATION_TOKEN_HEADER =
063      "X-Hadoop-Delegation-Token";
064
065  public static final String DELEGATION_PARAM = "delegation";
066  public static final String TOKEN_PARAM = "token";
067  public static final String RENEWER_PARAM = "renewer";
068  public static final String DELEGATION_TOKEN_JSON = "Token";
069  public static final String DELEGATION_TOKEN_URL_STRING_JSON = "urlString";
070  public static final String RENEW_DELEGATION_TOKEN_JSON = "long";
071
072  /**
073   * DelegationToken operations.
074   */
075  @InterfaceAudience.Private
076  public static enum DelegationTokenOperation {
077    GETDELEGATIONTOKEN(HTTP_GET, true),
078    RENEWDELEGATIONTOKEN(HTTP_PUT, true),
079    CANCELDELEGATIONTOKEN(HTTP_PUT, false);
080
081    private String httpMethod;
082    private boolean requiresKerberosCredentials;
083
084    private DelegationTokenOperation(String httpMethod,
085        boolean requiresKerberosCredentials) {
086      this.httpMethod = httpMethod;
087      this.requiresKerberosCredentials = requiresKerberosCredentials;
088    }
089
090    public String getHttpMethod() {
091      return httpMethod;
092    }
093
094    public boolean requiresKerberosCredentials() {
095      return requiresKerberosCredentials;
096    }
097  }
098
099  private Authenticator authenticator;
100  private ConnectionConfigurator connConfigurator;
101
102  public DelegationTokenAuthenticator(Authenticator authenticator) {
103    this.authenticator = authenticator;
104  }
105
106  @Override
107  public void setConnectionConfigurator(ConnectionConfigurator configurator) {
108    authenticator.setConnectionConfigurator(configurator);
109    connConfigurator = configurator;
110  }
111
112  private boolean hasDelegationToken(URL url, AuthenticatedURL.Token token) {
113    boolean hasDt = false;
114    if (token instanceof DelegationTokenAuthenticatedURL.Token) {
115      hasDt = ((DelegationTokenAuthenticatedURL.Token) token).
116          getDelegationToken() != null;
117      if (hasDt) {
118        LOG.trace("Delegation token found: {}",
119            ((DelegationTokenAuthenticatedURL.Token) token)
120                .getDelegationToken());
121      }
122    }
123    if (!hasDt) {
124      String queryStr = url.getQuery();
125      hasDt = (queryStr != null) && queryStr.contains(DELEGATION_PARAM + "=");
126      LOG.trace("hasDt={}, queryStr={}", hasDt, queryStr);
127    }
128    return hasDt;
129  }
130
131  @Override
132  public void authenticate(URL url, AuthenticatedURL.Token token)
133      throws IOException, AuthenticationException {
134    if (!hasDelegationToken(url, token)) {
135      // check and renew TGT to handle potential expiration
136      UserGroupInformation.getCurrentUser().checkTGTAndReloginFromKeytab();
137      LOG.debug("No delegation token found for url={}, token={}, authenticating"
138          + " with {}", url, token, authenticator.getClass());
139      authenticator.authenticate(url, token);
140    } else {
141      LOG.debug("Authenticated from delegation token. url={}, token={}",
142          url, token);
143    }
144  }
145
146  /**
147   * Requests a delegation token using the configured <code>Authenticator</code>
148   * for authentication.
149   *
150   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
151   * supported.
152   * @param token the authentication token being used for the user where the
153   * Delegation token will be stored.
154   * @param renewer the renewer user.
155   * @throws IOException if an IO error occurred.
156   * @throws AuthenticationException if an authentication exception occurred.
157   */
158  public Token<AbstractDelegationTokenIdentifier> getDelegationToken(URL url,
159      AuthenticatedURL.Token token, String renewer)
160      throws IOException, AuthenticationException {
161   return getDelegationToken(url, token, renewer, null);
162  }
163
164  /**
165   * Requests a delegation token using the configured <code>Authenticator</code>
166   * for authentication.
167   *
168   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
169   * supported.
170   * @param token the authentication token being used for the user where the
171   * Delegation token will be stored.
172   * @param renewer the renewer user.
173   * @param doAsUser the user to do as, which will be the token owner.
174   * @throws IOException if an IO error occurred.
175   * @throws AuthenticationException if an authentication exception occurred.
176   */
177  public Token<AbstractDelegationTokenIdentifier> getDelegationToken(URL url,
178      AuthenticatedURL.Token token, String renewer, String doAsUser)
179      throws IOException, AuthenticationException {
180    Map json = doDelegationTokenOperation(url, token,
181        DelegationTokenOperation.GETDELEGATIONTOKEN, renewer, null, true,
182        doAsUser);
183    json = (Map) json.get(DELEGATION_TOKEN_JSON);
184    String tokenStr = (String) json.get(DELEGATION_TOKEN_URL_STRING_JSON);
185    Token<AbstractDelegationTokenIdentifier> dToken =
186        new Token<AbstractDelegationTokenIdentifier>();
187    dToken.decodeFromUrlString(tokenStr);
188    InetSocketAddress service = new InetSocketAddress(url.getHost(),
189        url.getPort());
190    SecurityUtil.setTokenService(dToken, service);
191    return dToken;
192  }
193
194  /**
195   * Renews a delegation token from the server end-point using the
196   * configured <code>Authenticator</code> for authentication.
197   *
198   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
199   * supported.
200   * @param token the authentication token with the Delegation Token to renew.
201   * @throws IOException if an IO error occurred.
202   * @throws AuthenticationException if an authentication exception occurred.
203   */
204  public long renewDelegationToken(URL url,
205      AuthenticatedURL.Token token,
206      Token<AbstractDelegationTokenIdentifier> dToken)
207      throws IOException, AuthenticationException {
208    return renewDelegationToken(url, token, dToken, null);
209  }
210
211  /**
212   * Renews a delegation token from the server end-point using the
213   * configured <code>Authenticator</code> for authentication.
214   *
215   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
216   * supported.
217   * @param token the authentication token with the Delegation Token to renew.
218   * @param doAsUser the user to do as, which will be the token owner.
219   * @throws IOException if an IO error occurred.
220   * @throws AuthenticationException if an authentication exception occurred.
221   */
222  public long renewDelegationToken(URL url,
223      AuthenticatedURL.Token token,
224      Token<AbstractDelegationTokenIdentifier> dToken, String doAsUser)
225      throws IOException, AuthenticationException {
226    Map json = doDelegationTokenOperation(url, token,
227        DelegationTokenOperation.RENEWDELEGATIONTOKEN, null, dToken, true,
228        doAsUser);
229    return (Long) json.get(RENEW_DELEGATION_TOKEN_JSON);
230  }
231
232  /**
233   * Cancels a delegation token from the server end-point. It does not require
234   * being authenticated by the configured <code>Authenticator</code>.
235   *
236   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
237   * are supported.
238   * @param token the authentication token with the Delegation Token to cancel.
239   * @throws IOException if an IO error occurred.
240   */
241  public void cancelDelegationToken(URL url,
242      AuthenticatedURL.Token token,
243      Token<AbstractDelegationTokenIdentifier> dToken)
244      throws IOException {
245    cancelDelegationToken(url, token, dToken, null);
246  }
247
248  /**
249   * Cancels a delegation token from the server end-point. It does not require
250   * being authenticated by the configured <code>Authenticator</code>.
251   *
252   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
253   * are supported.
254   * @param token the authentication token with the Delegation Token to cancel.
255   * @param doAsUser the user to do as, which will be the token owner.
256   * @throws IOException if an IO error occurred.
257   */
258  public void cancelDelegationToken(URL url,
259      AuthenticatedURL.Token token,
260      Token<AbstractDelegationTokenIdentifier> dToken, String doAsUser)
261      throws IOException {
262    try {
263      doDelegationTokenOperation(url, token,
264          DelegationTokenOperation.CANCELDELEGATIONTOKEN, null, dToken, false,
265          doAsUser);
266    } catch (AuthenticationException ex) {
267      throw new IOException("This should not happen: " + ex.getMessage(), ex);
268    }
269  }
270
271  private Map doDelegationTokenOperation(URL url,
272      AuthenticatedURL.Token token, DelegationTokenOperation operation,
273      String renewer, Token<?> dToken, boolean hasResponse, String doAsUser)
274      throws IOException, AuthenticationException {
275    Map ret = null;
276    Map<String, String> params = new HashMap<String, String>();
277    params.put(OP_PARAM, operation.toString());
278    if (renewer != null) {
279      params.put(RENEWER_PARAM, renewer);
280    }
281    if (dToken != null) {
282      params.put(TOKEN_PARAM, dToken.encodeToUrlString());
283    }
284    // proxyuser
285    if (doAsUser != null) {
286      params.put(DelegationTokenAuthenticatedURL.DO_AS,
287          URLEncoder.encode(doAsUser, "UTF-8"));
288    }
289    String urlStr = url.toExternalForm();
290    StringBuilder sb = new StringBuilder(urlStr);
291    String separator = (urlStr.contains("?")) ? "&" : "?";
292    for (Map.Entry<String, String> entry : params.entrySet()) {
293      sb.append(separator).append(entry.getKey()).append("=").
294          append(URLEncoder.encode(entry.getValue(), "UTF8"));
295      separator = "&";
296    }
297    url = new URL(sb.toString());
298    AuthenticatedURL aUrl = new AuthenticatedURL(this, connConfigurator);
299    org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
300        dt = null;
301    if (token instanceof DelegationTokenAuthenticatedURL.Token
302        && operation.requiresKerberosCredentials()) {
303      // Unset delegation token to trigger fall-back authentication.
304      dt = ((DelegationTokenAuthenticatedURL.Token) token).getDelegationToken();
305      ((DelegationTokenAuthenticatedURL.Token) token).setDelegationToken(null);
306    }
307    try {
308      HttpURLConnection conn = aUrl.openConnection(url, token);
309      conn.setRequestMethod(operation.getHttpMethod());
310      HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
311      if (hasResponse) {
312        String contentType = conn.getHeaderField(CONTENT_TYPE);
313        contentType = (contentType != null) ? contentType.toLowerCase()
314            : null;
315        if (contentType != null &&
316            contentType.contains(APPLICATION_JSON_MIME)) {
317          try {
318            ObjectMapper mapper = new ObjectMapper();
319            ret = mapper.readValue(conn.getInputStream(), Map.class);
320          } catch (Exception ex) {
321            throw new AuthenticationException(String.format(
322                "'%s' did not handle the '%s' delegation token operation: %s",
323                url.getAuthority(), operation, ex.getMessage()), ex);
324          }
325        } else {
326          throw new AuthenticationException(String.format("'%s' did not " +
327                  "respond with JSON to the '%s' delegation token operation",
328              url.getAuthority(), operation));
329        }
330      }
331    } finally {
332      if (dt != null) {
333        ((DelegationTokenAuthenticatedURL.Token) token).setDelegationToken(dt);
334      }
335    }
336    return ret;
337  }
338
339}