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.apache.hadoop.util.StringUtils;
032import org.codehaus.jackson.map.ObjectMapper;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import java.io.IOException;
037import java.net.HttpURLConnection;
038import java.net.InetSocketAddress;
039import java.net.URL;
040import java.net.URLEncoder;
041import java.util.HashMap;
042import java.util.Map;
043
044/**
045 * {@link Authenticator} wrapper that enhances an {@link Authenticator} with
046 * Delegation Token support.
047 */
048@InterfaceAudience.Public
049@InterfaceStability.Evolving
050public abstract class DelegationTokenAuthenticator implements Authenticator {
051  private static Logger LOG = 
052      LoggerFactory.getLogger(DelegationTokenAuthenticator.class);
053  
054  private static final String CONTENT_TYPE = "Content-Type";
055  private static final String APPLICATION_JSON_MIME = "application/json";
056
057  private static final String HTTP_GET = "GET";
058  private static final String HTTP_PUT = "PUT";
059
060  public static final String OP_PARAM = "op";
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    }
118    if (!hasDt) {
119      String queryStr = url.getQuery();
120      hasDt = (queryStr != null) && queryStr.contains(DELEGATION_PARAM + "=");
121    }
122    return hasDt;
123  }
124
125  /**
126   * Append the delegation token to the request header if needed.
127   */
128  private void appendDelegationToken(final AuthenticatedURL.Token token,
129      final Token<?> dToken, final HttpURLConnection conn) throws IOException {
130    if (token.isSet()) {
131      LOG.debug("Auth token is set, not appending delegation token.");
132      return;
133    }
134    if (dToken == null) {
135      LOG.warn("Delegation token is null, cannot set on request header.");
136      return;
137    }
138    conn.setRequestProperty(
139        DelegationTokenAuthenticator.DELEGATION_TOKEN_HEADER,
140        dToken.encodeToUrlString());
141  }
142
143  @Override
144  public void authenticate(URL url, AuthenticatedURL.Token token)
145      throws IOException, AuthenticationException {
146    if (!hasDelegationToken(url, token)) {
147      // check and renew TGT to handle potential expiration
148      UserGroupInformation.getCurrentUser().checkTGTAndReloginFromKeytab();
149      authenticator.authenticate(url, token);
150    }
151  }
152
153  /**
154   * Requests a delegation token using the configured <code>Authenticator</code>
155   * for authentication.
156   *
157   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
158   * supported.
159   * @param token the authentication token being used for the user where the
160   * Delegation token will be stored.
161   * @param renewer the renewer user.
162   * @throws IOException if an IO error occurred.
163   * @throws AuthenticationException if an authentication exception occurred.
164   */
165  public Token<AbstractDelegationTokenIdentifier> getDelegationToken(URL url,
166      AuthenticatedURL.Token token, String renewer)
167      throws IOException, AuthenticationException {
168   return getDelegationToken(url, token, renewer, null);
169  }
170
171  /**
172   * Requests a delegation token using the configured <code>Authenticator</code>
173   * for authentication.
174   *
175   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
176   * supported.
177   * @param token the authentication token being used for the user where the
178   * Delegation token will be stored.
179   * @param renewer the renewer user.
180   * @param doAsUser the user to do as, which will be the token owner.
181   * @throws IOException if an IO error occurred.
182   * @throws AuthenticationException if an authentication exception occurred.
183   */
184  public Token<AbstractDelegationTokenIdentifier> getDelegationToken(URL url,
185      AuthenticatedURL.Token token, String renewer, String doAsUser)
186      throws IOException, AuthenticationException {
187    Map json = doDelegationTokenOperation(url, token,
188        DelegationTokenOperation.GETDELEGATIONTOKEN, renewer, null, true,
189        doAsUser);
190    json = (Map) json.get(DELEGATION_TOKEN_JSON);
191    String tokenStr = (String) json.get(DELEGATION_TOKEN_URL_STRING_JSON);
192    Token<AbstractDelegationTokenIdentifier> dToken =
193        new Token<AbstractDelegationTokenIdentifier>();
194    dToken.decodeFromUrlString(tokenStr);
195    InetSocketAddress service = new InetSocketAddress(url.getHost(),
196        url.getPort());
197    SecurityUtil.setTokenService(dToken, service);
198    return dToken;
199  }
200
201  /**
202   * Renews a delegation token from the server end-point using the
203   * configured <code>Authenticator</code> for authentication.
204   *
205   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
206   * supported.
207   * @param token the authentication token with the Delegation Token to renew.
208   * @throws IOException if an IO error occurred.
209   * @throws AuthenticationException if an authentication exception occurred.
210   */
211  public long renewDelegationToken(URL url,
212      AuthenticatedURL.Token token,
213      Token<AbstractDelegationTokenIdentifier> dToken)
214      throws IOException, AuthenticationException {
215    return renewDelegationToken(url, token, dToken, null);
216  }
217
218  /**
219   * Renews a delegation token from the server end-point using the
220   * configured <code>Authenticator</code> for authentication.
221   *
222   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
223   * supported.
224   * @param token the authentication token with the Delegation Token to renew.
225   * @param doAsUser the user to do as, which will be the token owner.
226   * @throws IOException if an IO error occurred.
227   * @throws AuthenticationException if an authentication exception occurred.
228   */
229  public long renewDelegationToken(URL url,
230      AuthenticatedURL.Token token,
231      Token<AbstractDelegationTokenIdentifier> dToken, String doAsUser)
232      throws IOException, AuthenticationException {
233    Map json = doDelegationTokenOperation(url, token,
234        DelegationTokenOperation.RENEWDELEGATIONTOKEN, null, dToken, true,
235        doAsUser);
236    return (Long) json.get(RENEW_DELEGATION_TOKEN_JSON);
237  }
238
239  /**
240   * Cancels a delegation token from the server end-point. It does not require
241   * being authenticated by the configured <code>Authenticator</code>.
242   *
243   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
244   * are supported.
245   * @param token the authentication token with the Delegation Token to cancel.
246   * @throws IOException if an IO error occurred.
247   */
248  public void cancelDelegationToken(URL url,
249      AuthenticatedURL.Token token,
250      Token<AbstractDelegationTokenIdentifier> dToken)
251      throws IOException {
252    cancelDelegationToken(url, token, dToken, null);
253  }
254
255  /**
256   * Cancels a delegation token from the server end-point. It does not require
257   * being authenticated by the configured <code>Authenticator</code>.
258   *
259   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
260   * are supported.
261   * @param token the authentication token with the Delegation Token to cancel.
262   * @param doAsUser the user to do as, which will be the token owner.
263   * @throws IOException if an IO error occurred.
264   */
265  public void cancelDelegationToken(URL url,
266      AuthenticatedURL.Token token,
267      Token<AbstractDelegationTokenIdentifier> dToken, String doAsUser)
268      throws IOException {
269    try {
270      doDelegationTokenOperation(url, token,
271          DelegationTokenOperation.CANCELDELEGATIONTOKEN, null, dToken, false,
272          doAsUser);
273    } catch (AuthenticationException ex) {
274      throw new IOException("This should not happen: " + ex.getMessage(), ex);
275    }
276  }
277
278  private Map doDelegationTokenOperation(URL url,
279      AuthenticatedURL.Token token, DelegationTokenOperation operation,
280      String renewer, Token<?> dToken, boolean hasResponse, String doAsUser)
281      throws IOException, AuthenticationException {
282    Map ret = null;
283    Map<String, String> params = new HashMap<String, String>();
284    params.put(OP_PARAM, operation.toString());
285    if (renewer != null) {
286      params.put(RENEWER_PARAM, renewer);
287    }
288    if (dToken != null) {
289      params.put(TOKEN_PARAM, dToken.encodeToUrlString());
290    }
291    // proxyuser
292    if (doAsUser != null) {
293      params.put(DelegationTokenAuthenticatedURL.DO_AS,
294          URLEncoder.encode(doAsUser, "UTF-8"));
295    }
296    String urlStr = url.toExternalForm();
297    StringBuilder sb = new StringBuilder(urlStr);
298    String separator = (urlStr.contains("?")) ? "&" : "?";
299    for (Map.Entry<String, String> entry : params.entrySet()) {
300      sb.append(separator).append(entry.getKey()).append("=").
301          append(URLEncoder.encode(entry.getValue(), "UTF8"));
302      separator = "&";
303    }
304    url = new URL(sb.toString());
305    AuthenticatedURL aUrl = new AuthenticatedURL(this, connConfigurator);
306    HttpURLConnection conn = aUrl.openConnection(url, token);
307    appendDelegationToken(token, dToken, conn);
308    conn.setRequestMethod(operation.getHttpMethod());
309    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
310    if (hasResponse) {
311      String contentType = conn.getHeaderField(CONTENT_TYPE);
312      contentType = (contentType != null) ? StringUtils.toLowerCase(contentType)
313                                          : null;
314      if (contentType != null &&
315          contentType.contains(APPLICATION_JSON_MIME)) {
316        try {
317          ObjectMapper mapper = new ObjectMapper();
318          ret = mapper.readValue(conn.getInputStream(), Map.class);
319        } catch (Exception ex) {
320          throw new AuthenticationException(String.format(
321              "'%s' did not handle the '%s' delegation token operation: %s",
322              url.getAuthority(), operation, ex.getMessage()), ex);
323        }
324      } else {
325        throw new AuthenticationException(String.format("'%s' did not " +
326                "respond with JSON to the '%s' delegation token operation",
327            url.getAuthority(), operation));
328      }
329    }
330    return ret;
331  }
332
333}