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    package org.apache.oozie.client;
019    
020    import java.io.BufferedReader;
021    import java.io.File;
022    import java.io.FileReader;
023    import java.io.FileWriter;
024    import java.io.IOException;
025    import java.io.Writer;
026    import java.net.HttpURLConnection;
027    import java.net.URL;
028    import java.util.HashMap;
029    import java.util.Map;
030    
031    import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
032    import org.apache.hadoop.security.authentication.client.AuthenticationException;
033    import org.apache.hadoop.security.authentication.client.Authenticator;
034    import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
035    import org.apache.hadoop.security.authentication.client.PseudoAuthenticator;
036    
037    /**
038     * This subclass of {@link XOozieClient} supports Kerberos HTTP SPNEGO and simple authentication.
039     */
040    public 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    }