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 */ 018 019package org.apache.oozie.client; 020 021import java.io.BufferedReader; 022import java.io.File; 023import java.io.FileReader; 024import java.io.FileWriter; 025import java.io.IOException; 026import java.io.Writer; 027import java.lang.management.ManagementFactory; 028import java.net.HttpURLConnection; 029import java.net.URL; 030import java.nio.file.Files; 031import java.nio.file.StandardCopyOption; 032import java.util.HashMap; 033import java.util.Map; 034 035import org.apache.hadoop.security.authentication.client.AuthenticatedURL; 036import org.apache.hadoop.security.authentication.client.AuthenticationException; 037import org.apache.hadoop.security.authentication.client.Authenticator; 038import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; 039import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; 040 041/** 042 * This subclass of {@link XOozieClient} supports Kerberos HTTP SPNEGO and simple authentication. 043 */ 044public class AuthOozieClient extends XOozieClient { 045 046 /** 047 * Java system property to specify a custom Authenticator implementation. 048 */ 049 public static final String AUTHENTICATOR_CLASS_SYS_PROP = "authenticator.class"; 050 051 /** 052 * Java system property that, if set the authentication token will be cached in the user home directory in a hidden 053 * file <code>.oozie-auth-token</code> with user read/write permissions only. 054 */ 055 public static final String USE_AUTH_TOKEN_CACHE_SYS_PROP = "oozie.auth.token.cache"; 056 057 /** 058 * File constant that defines the location of the authentication token cache file. 059 * <p> 060 * It resolves to <code>${user.home}/.oozie-auth-token</code>. 061 */ 062 public static final File AUTH_TOKEN_CACHE_FILE = new File(System.getProperty("user.home"), ".oozie-auth-token"); 063 064 public static enum AuthType { 065 KERBEROS, SIMPLE 066 } 067 068 private String authOption = null; 069 070 /** 071 * Create an instance of the AuthOozieClient. 072 * 073 * @param oozieUrl the Oozie URL 074 */ 075 public AuthOozieClient(String oozieUrl) { 076 this(oozieUrl, null); 077 } 078 079 /** 080 * Create an instance of the AuthOozieClient. 081 * 082 * @param oozieUrl the Oozie URL 083 * @param authOption the auth option 084 */ 085 public AuthOozieClient(String oozieUrl, String authOption) { 086 super(oozieUrl); 087 this.authOption = authOption; 088 } 089 090 /** 091 * Create an authenticated connection to the Oozie server. 092 * <p> 093 * It uses Hadoop-auth client authentication which by default supports 094 * Kerberos HTTP SPNEGO, Pseudo/Simple and anonymous. 095 * <p> 096 * if the Java system property {@link #USE_AUTH_TOKEN_CACHE_SYS_PROP} is set to true Hadoop-auth 097 * authentication token will be cached/used in/from the '.oozie-auth-token' file in the user 098 * home directory. 099 * 100 * @param url the URL to open a HTTP connection to. 101 * @param method the HTTP method for the HTTP connection. 102 * @return an authenticated connection to the Oozie server. 103 * @throws IOException if an IO error occurred. 104 * @throws OozieClientException if an oozie client error occurred. 105 */ 106 @Override 107 protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException { 108 boolean useAuthFile = System.getProperty(USE_AUTH_TOKEN_CACHE_SYS_PROP, "false").equalsIgnoreCase("true"); 109 AuthenticatedURL.Token readToken = null; 110 AuthenticatedURL.Token currentToken = null; 111 112 // Read the token in from the file 113 if (useAuthFile) { 114 readToken = readAuthToken(); 115 } 116 if (readToken == null) { 117 currentToken = new AuthenticatedURL.Token(); 118 } else { 119 currentToken = new AuthenticatedURL.Token(readToken.toString()); 120 } 121 122 // To prevent rare race conditions and to save a call to the Server, lets check the token's expiration time locally, and 123 // consider it expired if its expiration time has passed or will pass in the next 5 minutes (or if there's a problem parsing 124 // it) 125 if (currentToken.isSet()) { 126 long expires = getExpirationTime(currentToken); 127 if (expires < System.currentTimeMillis() + 300000) { 128 if (useAuthFile) { 129 AUTH_TOKEN_CACHE_FILE.delete(); 130 } 131 currentToken = new AuthenticatedURL.Token(); 132 } 133 } 134 135 // If we have a token, double check with the Server to make sure it hasn't expired yet 136 if (currentToken.isSet()) { 137 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 138 conn.setRequestMethod("OPTIONS"); 139 AuthenticatedURL.injectToken(conn, currentToken); 140 if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED 141 || conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { 142 if (useAuthFile) { 143 AUTH_TOKEN_CACHE_FILE.delete(); 144 } 145 currentToken = new AuthenticatedURL.Token(); 146 } else { 147 // After HADOOP-10301, with Kerberos the above token expiration check will now send 200 even with an expired token 148 // if you still have valid Kerberos credentials. Previously, it would send 401 so the client knows that it needs to 149 // use the KerberosAuthenticator to get a new token. Now, it may even provide a token back from this call, so we 150 // need to check for a new token and update ours. If no new token was given and we got a 20X code, this will do a 151 // no-op. 152 // With Pseudo, the above token expiration check will now send 403 instead of the 401; we're now checking for either 153 // response code above. However, unlike with Kerberos, Pseudo doesn't give us a new token here; we'll have to get 154 // one later. 155 try { 156 AuthenticatedURL.extractToken(conn, currentToken); 157 } catch (AuthenticationException ex) { 158 if (useAuthFile) { 159 AUTH_TOKEN_CACHE_FILE.delete(); 160 } 161 currentToken = new AuthenticatedURL.Token(); 162 } 163 } 164 } 165 166 // If we didn't have a token, or it had expired, let's get a new one from the Server using the configured Authenticator 167 if (!currentToken.isSet()) { 168 Authenticator authenticator = getAuthenticator(); 169 try { 170 authenticator.authenticate(url, currentToken); 171 } 172 catch (AuthenticationException ex) { 173 if (useAuthFile) { 174 AUTH_TOKEN_CACHE_FILE.delete(); 175 } 176 throw new OozieClientException(OozieClientException.AUTHENTICATION, 177 "Could not authenticate, " + ex.getMessage(), ex); 178 } 179 } 180 181 // If we got a new token, save it to the cache file 182 if (useAuthFile && currentToken.isSet() && !currentToken.equals(readToken)) { 183 writeAuthToken(currentToken); 184 } 185 186 // Now create a connection using the token and return it to the caller 187 HttpURLConnection conn = super.createConnection(url, method); 188 AuthenticatedURL.injectToken(conn, currentToken); 189 return conn; 190 } 191 192 private static long getExpirationTime(AuthenticatedURL.Token token) { 193 long expires = 0L; 194 String[] splits = token.toString().split("&"); 195 for (String split : splits) { 196 if (split.startsWith("e=")) { 197 try { 198 expires = Long.parseLong(split.substring(2)); 199 } catch (Exception e) { 200 // token is somehow invalid, assume it expired already 201 break; 202 } 203 } 204 } 205 return expires; 206 } 207 208 /** 209 * Read a authentication token cached in the user home directory. 210 * <p> 211 * 212 * @return the authentication token cached in the user home directory, NULL if none. 213 */ 214 protected AuthenticatedURL.Token readAuthToken() { 215 AuthenticatedURL.Token authToken = null; 216 if (AUTH_TOKEN_CACHE_FILE.exists()) { 217 try { 218 BufferedReader reader = new BufferedReader(new FileReader(AUTH_TOKEN_CACHE_FILE)); 219 String line = reader.readLine(); 220 reader.close(); 221 if (line != null) { 222 authToken = new AuthenticatedURL.Token(line); 223 } 224 } 225 catch (IOException ex) { 226 //NOP 227 } 228 } 229 return authToken; 230 } 231 232 /** 233 * Write the current authentication token to the user home directory.authOption 234 * <p> 235 * The file is written with user only read/write permissions. 236 * <p> 237 * If the file cannot be updated or the user only ready/write permissions cannot be set the file is deleted. 238 * 239 * @param authToken the authentication token to cache. 240 */ 241 protected void writeAuthToken(AuthenticatedURL.Token authToken) { 242 try { 243 String jvmName = ManagementFactory.getRuntimeMXBean().getName(); 244 File tmpTokenFile = File.createTempFile(".oozie-auth-token", jvmName + "tmp", 245 new File(System.getProperty("user.home"))); 246 // just to be safe, if something goes wrong delete tmp file eventually 247 tmpTokenFile.deleteOnExit(); 248 Writer writer = new FileWriter(tmpTokenFile); 249 writer.write(authToken.toString()); 250 writer.close(); 251 Files.move(tmpTokenFile.toPath(), AUTH_TOKEN_CACHE_FILE.toPath(), StandardCopyOption.ATOMIC_MOVE); 252 // sets read-write permissions to owner only 253 AUTH_TOKEN_CACHE_FILE.setReadable(false, false); 254 AUTH_TOKEN_CACHE_FILE.setReadable(true, true); 255 AUTH_TOKEN_CACHE_FILE.setWritable(true, true); 256 } 257 catch (IOException ioe) { 258 // if case of any error we just delete the cache, if user-only 259 // write permissions are not properly set a security exception 260 // is thrown and the file will be deleted. 261 AUTH_TOKEN_CACHE_FILE.delete(); 262 263 } 264 } 265 266 /** 267 * Return the Hadoop-auth Authenticator to use. 268 * <p> 269 * It first looks for value of command line option 'auth', if not set it continues to check 270 * {@link #AUTHENTICATOR_CLASS_SYS_PROP} Java system property for Authenticator. 271 * <p> 272 * It the value of the {@link #AUTHENTICATOR_CLASS_SYS_PROP} is not set it uses 273 * Hadoop-auth <code>KerberosAuthenticator</code> which supports both Kerberos HTTP SPNEGO and Pseudo/simple 274 * authentication. 275 * 276 * @return the Authenticator to use, <code>NULL</code> if none. 277 * 278 * @throws OozieClientException thrown if the authenticator could not be instantiated. 279 */ 280 protected Authenticator getAuthenticator() throws OozieClientException { 281 if (authOption != null) { 282 try { 283 Class<? extends Authenticator> authClass = getAuthenticators().get(authOption.toUpperCase()); 284 if (authClass == null) { 285 throw new OozieClientException(OozieClientException.AUTHENTICATION, 286 "Authenticator class not found [" + authClass + "]"); 287 } 288 return authClass.newInstance(); 289 } 290 catch (IllegalArgumentException iae) { 291 throw new OozieClientException(OozieClientException.AUTHENTICATION, "Invalid options provided for auth: " + authOption 292 + ", (" + AuthType.KERBEROS + " or " + AuthType.SIMPLE + " expected.)"); 293 } 294 catch (InstantiationException ex) { 295 throw new OozieClientException(OozieClientException.AUTHENTICATION, 296 "Could not instantiate Authenticator for option [" + authOption + "], " + 297 ex.getMessage(), ex); 298 } 299 catch (IllegalAccessException ex) { 300 throw new OozieClientException(OozieClientException.AUTHENTICATION, 301 "Could not instantiate Authenticator for option [" + authOption + "], " + 302 ex.getMessage(), ex); 303 } 304 305 } 306 307 String className = System.getProperty(AUTHENTICATOR_CLASS_SYS_PROP, KerberosAuthenticator.class.getName()); 308 if (className != null) { 309 try { 310 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 311 Class<? extends Object> klass = (cl != null) ? cl.loadClass(className) : 312 getClass().getClassLoader().loadClass(className); 313 if (klass == null) { 314 throw new OozieClientException(OozieClientException.AUTHENTICATION, 315 "Authenticator class not found [" + className + "]"); 316 } 317 return (Authenticator) klass.newInstance(); 318 } 319 catch (Exception ex) { 320 throw new OozieClientException(OozieClientException.AUTHENTICATION, 321 "Could not instantiate Authenticator [" + className + "], " + 322 ex.getMessage(), ex); 323 } 324 } 325 else { 326 throw new OozieClientException(OozieClientException.AUTHENTICATION, 327 "Authenticator class not found [" + className + "]"); 328 } 329 } 330 331 /** 332 * Get the map for classes of Authenticator. 333 * Default values are: 334 * null : KerberosAuthenticator 335 * SIMPLE : PseudoAuthenticator 336 * KERBEROS : KerberosAuthenticator 337 * 338 * @return the map for classes of Authenticator 339 */ 340 protected Map<String, Class<? extends Authenticator>> getAuthenticators() { 341 Map<String, Class<? extends Authenticator>> authClasses = new HashMap<String, Class<? extends Authenticator>>(); 342 authClasses.put(AuthType.KERBEROS.toString(), KerberosAuthenticator.class); 343 authClasses.put(AuthType.SIMPLE.toString(), PseudoAuthenticator.class); 344 authClasses.put(null, KerberosAuthenticator.class); 345 return authClasses; 346 } 347 348 /** 349 * Get authOption 350 * 351 * @return the authOption 352 */ 353 public String getAuthOption() { 354 return authOption; 355 } 356 357}