001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.client;
016    
017    import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
018    import org.apache.hadoop.security.authentication.client.AuthenticationException;
019    import org.apache.hadoop.security.authentication.client.Authenticator;
020    import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
021    
022    import java.io.BufferedReader;
023    import java.io.File;
024    import java.io.FileReader;
025    import java.io.FileWriter;
026    import java.io.IOException;
027    import java.io.Writer;
028    import java.net.HttpURLConnection;
029    import java.net.URL;
030    
031    /**
032     * This subclass of {@link XOozieClient} supports Kerberos HTTP SPNEGO and simple authentication.
033     */
034    public class AuthOozieClient extends XOozieClient {
035    
036        /**
037         * Java system property to specify a custom Authenticator implementation.
038         */
039        public static final String AUTHENTICATOR_CLASS_SYS_PROP = "authenticator.class";
040    
041        /**
042         * Java system property that, if set the authentication token will be cached in the user home directory in a hidden
043         * file <code>.oozie-auth-token</code> with user read/write permissions only.
044         */
045        public static final String USE_AUTH_TOKEN_CACHE_SYS_PROP = "oozie.auth.token.cache";
046    
047        /**
048         * File constant that defines the location of the authentication token cache file.
049         * <p/>
050         * It resolves to <code>${user.home}/.oozie-auth-token</code>.
051         */
052        public static final File AUTH_TOKEN_CACHE_FILE = new File(System.getProperty("user.home"), ".oozie-auth-token");
053    
054        /**
055         * Create an instance of the AuthOozieClient.
056         *
057         * @param oozieUrl the Oozie URL
058         */
059        public AuthOozieClient(String oozieUrl) {
060            super(oozieUrl);
061        }
062    
063        /**
064         * Create an authenticated connection to the Oozie server.
065         * <p/>
066         * It uses Alfredo client authentication which by default supports
067         * Kerberos HTTP SPNEGO, Pseudo/Simple and anonymous.
068         * <p/>
069         * if the Java system property {@link #USE_AUTH_TOKEN_CACHE_SYS_PROP} is set to true Alfredo
070         * authentication token will be cached/used in/from the '.oozie-auth-token' file in the user
071         * home directory.
072         * 
073         * @param url the URL to open a HTTP connection to.
074         * @param method the HTTP method for the HTTP connection.
075         * @return an authenticated connection to the Oozie server.
076         * @throws IOException if an IO error occurred.
077         * @throws OozieClientException if an oozie client error occurred.
078         */
079        @Override
080        protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException {
081            boolean useAuthFile = System.getProperty(USE_AUTH_TOKEN_CACHE_SYS_PROP, "false").equalsIgnoreCase("true");
082            AuthenticatedURL.Token readToken = new AuthenticatedURL.Token();
083            AuthenticatedURL.Token currentToken = new AuthenticatedURL.Token();
084    
085            if (useAuthFile) {
086                readToken = readAuthToken();
087                if (readToken != null) {
088                    currentToken = new AuthenticatedURL.Token(readToken.toString());
089                }
090            }
091    
092            if (currentToken.isSet()) {
093                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
094                conn.setRequestMethod("OPTIONS");
095                AuthenticatedURL.injectToken(conn, currentToken);
096                if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
097                    AUTH_TOKEN_CACHE_FILE.delete();
098                    currentToken = new AuthenticatedURL.Token();
099                }
100            }
101    
102            if (!currentToken.isSet()) {
103                Authenticator authenticator = getAuthenticator();
104                try {
105                    new AuthenticatedURL(authenticator).openConnection(url, currentToken);
106                }
107                catch (AuthenticationException ex) {
108                    AUTH_TOKEN_CACHE_FILE.delete();
109                    throw new OozieClientException(OozieClientException.AUTHENTICATION,
110                                                   "Could not authenticate, " + ex.getMessage(), ex);
111                }
112            }
113            if (useAuthFile && !currentToken.equals(readToken)) {
114                writeAuthToken(currentToken);
115            }
116            HttpURLConnection conn = super.createConnection(url, method);
117    
118            AuthenticatedURL.injectToken(conn, currentToken);
119            return conn;
120        }
121    
122    
123        /**
124         * Read a authentication token cached in the user home directory.
125         * <p/>
126         *
127         * @return the authentication token cached in the user home directory, NULL if none.
128         */
129        protected AuthenticatedURL.Token readAuthToken() {
130            AuthenticatedURL.Token authToken = null;
131            if (AUTH_TOKEN_CACHE_FILE.exists()) {
132                try {
133                    BufferedReader reader = new BufferedReader(new FileReader(AUTH_TOKEN_CACHE_FILE));
134                    String line = reader.readLine();
135                    reader.close();
136                    if (line != null) {
137                        authToken = new AuthenticatedURL.Token(line);
138                    }
139                }
140                catch (IOException ex) {
141                    //NOP
142                }
143            }
144            return authToken;
145        }
146    
147        /**
148         * Write the current authenthication token to the user home directory.
149         * <p/>
150         * The file is written with user only read/write permissions.
151         * <p/>
152         * If the file cannot be updated or the user only ready/write permissions cannot be set the file is deleted.
153         *
154         * @param authToken the authentication token to cache.
155         */
156        protected void writeAuthToken(AuthenticatedURL.Token authToken) {
157            try {
158                Writer writer = new FileWriter(AUTH_TOKEN_CACHE_FILE);
159                writer.write(authToken.toString());
160                writer.close();
161                // sets read-write permissions to owner only
162                AUTH_TOKEN_CACHE_FILE.setReadable(false, false);
163                AUTH_TOKEN_CACHE_FILE.setReadable(true, true);
164                AUTH_TOKEN_CACHE_FILE.setWritable(true, true);
165            }
166            catch (Exception ex) {
167                // if case of any error we just delete the cache, if user-only
168                // write permissions are not properly set a security exception
169                // is thrown and the file will be deleted.
170                AUTH_TOKEN_CACHE_FILE.delete();
171            }
172        }
173    
174        /**
175         * Return the Alfredo Authenticator to use.
176         * <p/>
177         * It looks for value of the {@link #AUTHENTICATOR_CLASS_SYS_PROP} Java system property, if not set it uses Alfredo
178         * <code>KerberosAuthenticator</code> which supports both Kerberos HTTP SPNEGO and Pseudo/simple authentication.
179         *
180         * @return the Authenticator to use, <code>NULL</code> if none.
181         *
182         * @throws OozieClientException thrown if the authenticator could not be instatiated.
183         */
184        protected Authenticator getAuthenticator() throws OozieClientException {
185            String className = System.getProperty(AUTHENTICATOR_CLASS_SYS_PROP, KerberosAuthenticator.class.getName());
186            if (className != null) {
187                try {
188                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
189                    Class klass = (cl != null) ? cl.loadClass(className) : getClass().getClassLoader().loadClass(className);
190                    return (Authenticator) klass.newInstance();
191                }
192                catch (Exception ex) {
193                    throw new OozieClientException(OozieClientException.AUTHENTICATION,
194                                                   "Could not instantiate Authenticator [" + className + "], " +
195                                                   ex.getMessage(), ex);
196                }
197            }
198            else {
199                throw new OozieClientException(OozieClientException.AUTHENTICATION,
200                                               "Authenticator class not found [" + className + "]");
201            }
202        }
203    
204    }