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