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.net.HttpURLConnection;
028import java.net.URL;
029import java.util.HashMap;
030import java.util.Map;
031
032import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
033import org.apache.hadoop.security.authentication.client.AuthenticationException;
034import org.apache.hadoop.security.authentication.client.Authenticator;
035import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
036import org.apache.hadoop.security.authentication.client.PseudoAuthenticator;
037
038/**
039 * This subclass of {@link XOozieClient} supports Kerberos HTTP SPNEGO and simple authentication.
040 */
041public class AuthOozieClient extends XOozieClient {
042
043    /**
044     * Java system property to specify a custom Authenticator implementation.
045     */
046    public static final String AUTHENTICATOR_CLASS_SYS_PROP = "authenticator.class";
047
048    /**
049     * Java system property that, if set the authentication token will be cached in the user home directory in a hidden
050     * file <code>.oozie-auth-token</code> with user read/write permissions only.
051     */
052    public static final String USE_AUTH_TOKEN_CACHE_SYS_PROP = "oozie.auth.token.cache";
053
054    /**
055     * File constant that defines the location of the authentication token cache file.
056     * <p/>
057     * It resolves to <code>${user.home}/.oozie-auth-token</code>.
058     */
059    public static final File AUTH_TOKEN_CACHE_FILE = new File(System.getProperty("user.home"), ".oozie-auth-token");
060
061    public static enum AuthType {
062        KERBEROS, SIMPLE
063    }
064
065    private String authOption = null;
066
067    /**
068     * Create an instance of the AuthOozieClient.
069     *
070     * @param oozieUrl the Oozie URL
071     */
072    public AuthOozieClient(String oozieUrl) {
073        this(oozieUrl, null);
074    }
075
076    /**
077     * Create an instance of the AuthOozieClient.
078     *
079     * @param oozieUrl the Oozie URL
080     * @param authOption the auth option
081     */
082    public AuthOozieClient(String oozieUrl, String authOption) {
083        super(oozieUrl);
084        this.authOption = authOption;
085    }
086
087    /**
088     * Create an authenticated connection to the Oozie server.
089     * <p/>
090     * It uses Hadoop-auth client authentication which by default supports
091     * Kerberos HTTP SPNEGO, Pseudo/Simple and anonymous.
092     * <p/>
093     * if the Java system property {@link #USE_AUTH_TOKEN_CACHE_SYS_PROP} is set to true Hadoop-auth
094     * authentication token will be cached/used in/from the '.oozie-auth-token' file in the user
095     * home directory.
096     *
097     * @param url the URL to open a HTTP connection to.
098     * @param method the HTTP method for the HTTP connection.
099     * @return an authenticated connection to the Oozie server.
100     * @throws IOException if an IO error occurred.
101     * @throws OozieClientException if an oozie client error occurred.
102     */
103    @Override
104    protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException {
105        boolean useAuthFile = System.getProperty(USE_AUTH_TOKEN_CACHE_SYS_PROP, "false").equalsIgnoreCase("true");
106        AuthenticatedURL.Token readToken = new AuthenticatedURL.Token();
107        AuthenticatedURL.Token currentToken = new AuthenticatedURL.Token();
108
109        if (useAuthFile) {
110            readToken = readAuthToken();
111            if (readToken != null) {
112                currentToken = new AuthenticatedURL.Token(readToken.toString());
113            }
114        }
115
116        if (currentToken.isSet()) {
117            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
118            conn.setRequestMethod("OPTIONS");
119            AuthenticatedURL.injectToken(conn, currentToken);
120            if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
121                AUTH_TOKEN_CACHE_FILE.delete();
122                currentToken = new AuthenticatedURL.Token();
123            }
124        }
125
126        if (!currentToken.isSet()) {
127            Authenticator authenticator = getAuthenticator();
128            try {
129                new AuthenticatedURL(authenticator).openConnection(url, currentToken);
130            }
131            catch (AuthenticationException ex) {
132                AUTH_TOKEN_CACHE_FILE.delete();
133                throw new OozieClientException(OozieClientException.AUTHENTICATION,
134                                               "Could not authenticate, " + ex.getMessage(), ex);
135            }
136        }
137        if (useAuthFile && currentToken.isSet() && !currentToken.equals(readToken)) {
138            writeAuthToken(currentToken);
139        }
140        HttpURLConnection conn = super.createConnection(url, method);
141        AuthenticatedURL.injectToken(conn, currentToken);
142
143        return conn;
144    }
145
146
147    /**
148     * Read a authentication token cached in the user home directory.
149     * <p/>
150     *
151     * @return the authentication token cached in the user home directory, NULL if none.
152     */
153    protected AuthenticatedURL.Token readAuthToken() {
154        AuthenticatedURL.Token authToken = null;
155        if (AUTH_TOKEN_CACHE_FILE.exists()) {
156            try {
157                BufferedReader reader = new BufferedReader(new FileReader(AUTH_TOKEN_CACHE_FILE));
158                String line = reader.readLine();
159                reader.close();
160                if (line != null) {
161                    authToken = new AuthenticatedURL.Token(line);
162                }
163            }
164            catch (IOException ex) {
165                //NOP
166            }
167        }
168        return authToken;
169    }
170
171    /**
172     * Write the current authentication token to the user home directory.authOption
173     * <p/>
174     * The file is written with user only read/write permissions.
175     * <p/>
176     * If the file cannot be updated or the user only ready/write permissions cannot be set the file is deleted.
177     *
178     * @param authToken the authentication token to cache.
179     */
180    protected void writeAuthToken(AuthenticatedURL.Token authToken) {
181        try {
182            Writer writer = new FileWriter(AUTH_TOKEN_CACHE_FILE);
183            writer.write(authToken.toString());
184            writer.close();
185            // sets read-write permissions to owner only
186            AUTH_TOKEN_CACHE_FILE.setReadable(false, false);
187            AUTH_TOKEN_CACHE_FILE.setReadable(true, true);
188            AUTH_TOKEN_CACHE_FILE.setWritable(true, true);
189        }
190        catch (IOException ioe) {
191            // if case of any error we just delete the cache, if user-only
192            // write permissions are not properly set a security exception
193            // is thrown and the file will be deleted.
194            AUTH_TOKEN_CACHE_FILE.delete();
195        }
196    }
197
198    /**
199     * Return the Hadoop-auth Authenticator to use.
200     * <p/>
201     * It first looks for value of command line option 'auth', if not set it continues to check
202     * {@link #AUTHENTICATOR_CLASS_SYS_PROP} Java system property for Authenticator.
203     * <p/>
204     * It the value of the {@link #AUTHENTICATOR_CLASS_SYS_PROP} is not set it uses
205     * Hadoop-auth <code>KerberosAuthenticator</code> which supports both Kerberos HTTP SPNEGO and Pseudo/simple
206     * authentication.
207     *
208     * @return the Authenticator to use, <code>NULL</code> if none.
209     *
210     * @throws OozieClientException thrown if the authenticator could not be instantiated.
211     */
212    protected Authenticator getAuthenticator() throws OozieClientException {
213        if (authOption != null) {
214            try {
215                Class<? extends Authenticator> authClass = getAuthenticators().get(authOption.toUpperCase());
216                if (authClass == null) {
217                    throw new OozieClientException(OozieClientException.AUTHENTICATION,
218                            "Authenticator class not found [" + authClass + "]");
219                }
220                return authClass.newInstance();
221            }
222            catch (IllegalArgumentException iae) {
223                throw new OozieClientException(OozieClientException.AUTHENTICATION, "Invalid options provided for auth: " + authOption
224                        + ", (" + AuthType.KERBEROS + " or " + AuthType.SIMPLE + " expected.)");
225            }
226            catch (InstantiationException ex) {
227                throw new OozieClientException(OozieClientException.AUTHENTICATION,
228                        "Could not instantiate Authenticator for option [" + authOption + "], " +
229                        ex.getMessage(), ex);
230            }
231            catch (IllegalAccessException ex) {
232                throw new OozieClientException(OozieClientException.AUTHENTICATION,
233                        "Could not instantiate Authenticator for option [" + authOption + "], " +
234                        ex.getMessage(), ex);
235            }
236
237        }
238
239        String className = System.getProperty(AUTHENTICATOR_CLASS_SYS_PROP, KerberosAuthenticator.class.getName());
240        if (className != null) {
241            try {
242                ClassLoader cl = Thread.currentThread().getContextClassLoader();
243                Class<? extends Object> klass = (cl != null) ? cl.loadClass(className) :
244                    getClass().getClassLoader().loadClass(className);
245                if (klass == null) {
246                    throw new OozieClientException(OozieClientException.AUTHENTICATION,
247                            "Authenticator class not found [" + className + "]");
248                }
249                return (Authenticator) klass.newInstance();
250            }
251            catch (Exception ex) {
252                throw new OozieClientException(OozieClientException.AUTHENTICATION,
253                                               "Could not instantiate Authenticator [" + className + "], " +
254                                               ex.getMessage(), ex);
255            }
256        }
257        else {
258            throw new OozieClientException(OozieClientException.AUTHENTICATION,
259                                           "Authenticator class not found [" + className + "]");
260        }
261    }
262
263    /**
264     * Get the map for classes of Authenticator.
265     * Default values are:
266     * null -> KerberosAuthenticator
267     * SIMPLE -> PseudoAuthenticator
268     * KERBEROS -> KerberosAuthenticator
269     *
270     * @return the map for classes of Authenticator
271     * @throws OozieClientException
272     */
273    protected Map<String, Class<? extends Authenticator>> getAuthenticators() {
274        Map<String, Class<? extends Authenticator>> authClasses = new HashMap<String, Class<? extends Authenticator>>();
275        authClasses.put(AuthType.KERBEROS.toString(), KerberosAuthenticator.class);
276        authClasses.put(AuthType.SIMPLE.toString(), PseudoAuthenticator.class);
277        authClasses.put(null, KerberosAuthenticator.class);
278        return authClasses;
279    }
280
281    /**
282     * Get authOption
283     *
284     * @return the authOption
285     */
286    public String getAuthOption() {
287        return authOption;
288    }
289
290}