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}