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}