From 7a9ee30359f8b112f98851fc60b3ebd79fd500f9 Mon Sep 17 00:00:00 2001 From: Alejandro Abdelnur Date: Thu, 3 Feb 2011 14:32:25 +0800 Subject: [PATCH] Closes GH-0035 Add support for Kerberos HTTP SPNEGO authentication (using Alfredo) x --- client/pom.xml | 12 + client/src/main/bin/oozie | 3 +- .../org/apache/oozie/cli/AuthOozieCLI.java | 125 +++++++++ .../apache/oozie/client/AuthOozieClient.java | 216 ++++++++++++++++ core/pom.xml | 17 +- core/src/main/conf/oozie-log4j.properties | 1 + core/src/main/conf/oozie-site.xml | 66 +++++ .../org/apache/oozie/servlet/AuthFilter.java | 131 ++++++++++ core/src/main/resources/oozie-default.xml | 67 +++++ .../TestAuthFilterAuthOozieClient.java | 241 ++++++++++++++++++ docs/src/site/twiki/AG_Install.twiki | 59 ++++- docs/src/site/twiki/DG_CommandLineTool.twiki | 42 ++- pom.xml | 17 +- release-log.txt | 1 + webapp/pom.xml | 8 - webapp/src/main/webapp/WEB-INF/web.xml | 60 +++++ 16 files changed, 1038 insertions(+), 28 deletions(-) create mode 100644 client/src/main/java/org/apache/oozie/cli/AuthOozieCLI.java create mode 100644 client/src/main/java/org/apache/oozie/client/AuthOozieClient.java create mode 100644 core/src/main/java/org/apache/oozie/servlet/AuthFilter.java create mode 100644 core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java diff --git a/client/pom.xml b/client/pom.xml index cffe29fe0..cbc19a6b6 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -58,6 +58,18 @@ persistence-api provided + + + com.cloudera.alfredo + alfredo + compile + + + org.slf4j + slf4j-api + + + diff --git a/client/src/main/bin/oozie b/client/src/main/bin/oozie index c6d95d1b3..872d61c23 100644 --- a/client/src/main/bin/oozie +++ b/client/src/main/bin/oozie @@ -43,9 +43,8 @@ else JAVA_BIN=${JAVA_HOME}/bin/java fi -JAVA_PROPERTIES="" while [[ ${1} =~ ^\-D ]]; do JAVA_PROPERTIES="${JAVA_PROPERTIES} ${1}" shift done -${JAVA_BIN} ${JAVA_PROPERTIES} -cp ${OOZIECPPATH} org.apache.oozie.cli.OozieCLI "${@}" +${JAVA_BIN} ${JAVA_PROPERTIES} -cp ${OOZIECPPATH} org.apache.oozie.cli.AuthOozieCLI "${@}" diff --git a/client/src/main/java/org/apache/oozie/cli/AuthOozieCLI.java b/client/src/main/java/org/apache/oozie/cli/AuthOozieCLI.java new file mode 100644 index 000000000..e57ac7c99 --- /dev/null +++ b/client/src/main/java/org/apache/oozie/cli/AuthOozieCLI.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.oozie.cli; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.oozie.BuildInfo; +import org.apache.oozie.client.AuthOozieClient; +import org.apache.oozie.client.CoordinatorAction; +import org.apache.oozie.client.CoordinatorJob; +import org.apache.oozie.client.OozieClient; +import org.apache.oozie.client.OozieClient.SYSTEM_MODE; +import org.apache.oozie.client.OozieClientException; +import org.apache.oozie.client.WorkflowAction; +import org.apache.oozie.client.WorkflowJob; +import org.apache.oozie.client.XOozieClient; +import org.apache.oozie.client.rest.JsonCoordinatorAction; +import org.apache.oozie.client.rest.RestConstants; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +/** + * Oozie command line utility. + */ +public class AuthOozieCLI extends OozieCLI { + /** + * Entry point for the Oozie CLI when invoked from the command line. + *

+ * Upon completion this method exits the JVM with '0' (success) or '-1' + * (failure). + * + * @param args options and arguments for the Oozie CLI. + */ + public static void main(String[] args) { + if (!System.getProperties().contains(AuthOozieClient.USE_AUTH_TOKEN_CACHE_SYS_PROP)) { + System.setProperty(AuthOozieClient.USE_AUTH_TOKEN_CACHE_SYS_PROP, "true"); + } + System.exit(new AuthOozieCLI().run(args)); + } + + /** + * Create a OozieClient.

It injects any '-Dheader:' as header to the the {@link + * OozieClient}. + * + * @param commandLine the parsed command line options. + * @return a pre configured eXtended workflow client. + * @throws OozieCLIException thrown if the OozieClient could not be + * configured. + */ + @Override + protected OozieClient createOozieClient(CommandLine commandLine) throws OozieCLIException { + return createXOozieClient(commandLine); + } + + //TODO: This method should be made protected in OozieCLI so we don't have to reimplement it here + private void addHeader(OozieClient wc) { + for (Map.Entry entry : System.getProperties().entrySet()) { + String key = (String) entry.getKey(); + if (key.startsWith(WS_HEADER_PREFIX)) { + String header = key.substring(WS_HEADER_PREFIX.length()); + wc.setHeader(header, (String) entry.getValue()); + } + } + } + + /** + * Create a XOozieClient.

It injects any '-Dheader:' as header to the the {@link + * OozieClient}. + * + * @param commandLine the parsed command line options. + * @return a pre configured eXtended workflow client. + * @throws OozieCLIException thrown if the XOozieClient could not be + * configured. + */ + @Override + protected XOozieClient createXOozieClient(CommandLine commandLine) throws OozieCLIException { + XOozieClient wc = new AuthOozieClient(getOozieUrl(commandLine)); + addHeader(wc); + return wc; + } + +} diff --git a/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java b/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java new file mode 100644 index 000000000..4d50dd08a --- /dev/null +++ b/client/src/main/java/org/apache/oozie/client/AuthOozieClient.java @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.oozie.client; + +import com.cloudera.alfredo.client.AuthenticatedURL; +import com.cloudera.alfredo.client.AuthenticationException; +import com.cloudera.alfredo.client.Authenticator; +import com.cloudera.alfredo.client.KerberosAuthenticator; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * This subclass of {@link XOozieClient} supports Kerberos HTTP SPNEGO and simple authentication. + */ +public class AuthOozieClient extends XOozieClient { + + /** + * Java system property to specify a custom Authenticator implementation. + */ + public static final String AUTHENTICATOR_CLASS_SYS_PROP = "authenticator.class"; + + /** + * Java system property that, if set the authentication token will be cached in the user home directory in a hidden + * file .oozie-auth-token with user read/write permissions only. + */ + public static final String USE_AUTH_TOKEN_CACHE_SYS_PROP = "oozie.auth.token.cache"; + + /** + * File constant that defines the location of the authentication token cache file. + *

+ * It resolves to ${user.home}/.oozie-auth-token. + */ + public static final File AUTH_TOKEN_CACHE_FILE = new File(System.getProperty("user.home"), ".oozie-auth-token"); + + /** + * Create an instance of the AuthOozieClient. + * + * @param oozieUrl the Oozie URL + */ + public AuthOozieClient(String oozieUrl) { + super(oozieUrl); + } + + /** + * Create an authenticated connection to the Oozie server. + *

+ * It uses Alfredo client authentication which by default supports + * Kerberos HTTP SPNEGO, Pseudo/Simple and anonymous. + *

+ * if the Java system property {@link #USE_AUTH_TOKEN_CACHE_SYS_PROP} is set to true Alfredo + * authentication token will be cached/used in/from the '.oozie-auth-token' file in the user + * home directory. + * + * @param url the URL to open a HTTP connection to. + * @param method the HTTP method for the HTTP connection. + * @return an authenticated connection to the Oozie server. + * @throws IOException if an IO error occurred. + * @throws OozieClientException if an oozie client error occurred. + */ + @Override + protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException { + boolean useAuthFile = System.getProperty(USE_AUTH_TOKEN_CACHE_SYS_PROP, "false").equalsIgnoreCase("true"); + AuthenticatedURL.Token readToken = new AuthenticatedURL.Token(); + AuthenticatedURL.Token currentToken = new AuthenticatedURL.Token(); + + if (useAuthFile) { + readToken = readAuthToken(); + if (readToken != null) { + currentToken = new AuthenticatedURL.Token(readToken.toString()); + } + } + + if (currentToken.isSet()) { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("OPTIONS"); + injectToken(conn, currentToken); + if (conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { + AUTH_TOKEN_CACHE_FILE.delete(); + currentToken = new AuthenticatedURL.Token(); + } + } + + if (!currentToken.isSet()) { + Authenticator authenticator = getAuthenticator(); + try { + new AuthenticatedURL(authenticator).openConnection(url, currentToken); + } + catch (AuthenticationException ex) { + AUTH_TOKEN_CACHE_FILE.delete(); + throw new OozieClientException(OozieClientException.AUTHENTICATION, + "Could not authenticate, " + ex.getMessage(), ex); + } + } + if (useAuthFile && !currentToken.equals(readToken)) { + writeAuthToken(currentToken); + } + HttpURLConnection conn = super.createConnection(url, method); + + injectToken(conn, currentToken); + return conn; + } + + //TODO: replace this with AutheticatedURL.injectToken when the method is made public in Alfredo 0.1.5 + private void injectToken(HttpURLConnection conn, AuthenticatedURL.Token token) { + if (token.isSet()) { + String t = token.toString(); + if (t != null) { + if (!t.startsWith("\"")) { + t = "\"" + t + "\""; + } + conn.addRequestProperty("Cookie", AuthenticatedURL.AUTH_COOKIE + "=" + t); + } + } + } + + /** + * Read a authentication token cached in the user home directory. + *

+ * + * @return the authentication token cached in the user home directory, NULL if none. + */ + protected AuthenticatedURL.Token readAuthToken() { + AuthenticatedURL.Token authToken = null; + if (AUTH_TOKEN_CACHE_FILE.exists()) { + try { + BufferedReader reader = new BufferedReader(new FileReader(AUTH_TOKEN_CACHE_FILE)); + String line = reader.readLine(); + reader.close(); + if (line != null) { + authToken = new AuthenticatedURL.Token(line); + } + } + catch (IOException ex) { + //NOP + } + } + return authToken; + } + + /** + * Write the current authenthication token to the user home directory. + *

+ * The file is written with user only read/write permissions. + *

+ * If the file cannot be updated or the user only ready/write permissions cannot be set the file is deleted. + * + * @param authToken the authentication token to cache. + */ + protected void writeAuthToken(AuthenticatedURL.Token authToken) { + try { + Writer writer = new FileWriter(AUTH_TOKEN_CACHE_FILE); + writer.write(authToken.toString()); + writer.close(); + // sets read-write permissions to owner only + AUTH_TOKEN_CACHE_FILE.setReadable(false, false); + AUTH_TOKEN_CACHE_FILE.setReadable(true, true); + AUTH_TOKEN_CACHE_FILE.setWritable(true, true); + } + catch (Exception ex) { + // if case of any error we just delete the cache, if user-only + // write permissions are not properly set a security exception + // is thrown and the file will be deleted. + AUTH_TOKEN_CACHE_FILE.delete(); + } + } + + /** + * Return the Alfredo Authenticator to use. + *

+ * It looks for value of the {@link #AUTHENTICATOR_CLASS_SYS_PROP} Java system property, if not set it uses Alfredo + * KerberosAuthenticator which supports both Kerberos HTTP SPNEGO and Pseudo/simple authentication. + * + * @return the Authenticator to use, NULL if none. + * + * @throws OozieClientException thrown if the authenticator could not be instatiated. + */ + protected Authenticator getAuthenticator() throws OozieClientException { + String className = System.getProperty(AUTHENTICATOR_CLASS_SYS_PROP, KerberosAuthenticator.class.getName()); + if (className != null) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Class klass = (cl != null) ? cl.loadClass(className) : getClass().getClassLoader().loadClass(className); + return (Authenticator) klass.newInstance(); + } + catch (Exception ex) { + throw new OozieClientException(OozieClientException.AUTHENTICATION, + "Could not instantiate Authenticator [" + className + "], " + + ex.getMessage(), ex); + } + } + else { + throw new OozieClientException(OozieClientException.AUTHENTICATION, + "Authenticator class not found [" + className + "]"); + } + } + +} diff --git a/core/pom.xml b/core/pom.xml index 661fce131..a4e1ef1b6 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -94,12 +94,6 @@ provided - - org.slf4j - slf4j-log4j12 - test - - com.googlecode.json-simple json-simple @@ -169,6 +163,17 @@ compile + + com.cloudera.alfredo + alfredo + compile + + + + org.slf4j + slf4j-log4j12 + compile + diff --git a/core/src/main/conf/oozie-log4j.properties b/core/src/main/conf/oozie-log4j.properties index 3882d8abb..d4db27652 100644 --- a/core/src/main/conf/oozie-log4j.properties +++ b/core/src/main/conf/oozie-log4j.properties @@ -59,3 +59,4 @@ log4j.logger.org.apache.oozie=DEBUG, oozie log4j.logger.org.apache.hadoop=WARN, oozie log4j.logger.org.mortbay=WARN, oozie log4j.logger.org.hsqldb=WARN, oozie +log4j.logger.com.cloudera.alfredo=DEBUG, oozie diff --git a/core/src/main/conf/oozie-site.xml b/core/src/main/conf/oozie-site.xml index c0544376a..c32f0769c 100644 --- a/core/src/main/conf/oozie-site.xml +++ b/core/src/main/conf/oozie-site.xml @@ -228,5 +228,71 @@ + + oozie.authentication.type + simple + + Defines authentication used for Oozie HTTP endpoint. + Supported values are: simple | kerberos | #AUTHENTICATION_HANDLER_CLASSNAME# + + + + + oozie.authentication.token.validity + 36000 + + Indicates how long (in seconds) an authentication token is valid before it has + to be renewed. + + + + + oozie.authentication.signature.secret + oozie + + The signature secret for signing the authentication tokens. + If not set a random secret is generated at startup time. + In order to authentiation to work correctly across multiple hosts + the secret must be the same across al the hosts. + + + + + oozie.authentication.cookie.domain + + + The domain to use for the HTTP cookie that stores the authentication token. + In order to authentiation to work correctly across multiple hosts + the domain must be correctly set. + + + + + oozie.authentication.simple.anonymous.allowed + true + + Indicates if anonymous requests are allowed. + This setting is meaningful only when using 'simple' authentication. + + + + + oozie.authentication.kerberos.principal + HTTP/localhost@${local.realm} + + Indicates the Kerberos principal to be used for HTTP endpoint. + The principal MUST start with 'HTTP/' as per Kerberos HTTP SPNEGO specification. + + + + + oozie.authentication.kerberos.keytab + ${oozie.service.HadoopAccessorService.keytab.file} + + Location of the keytab file with the credentials for the principal. + Referring to the same keytab file Oozie uses for its Kerberos credentials for Hadoop. + + + diff --git a/core/src/main/java/org/apache/oozie/servlet/AuthFilter.java b/core/src/main/java/org/apache/oozie/servlet/AuthFilter.java new file mode 100644 index 000000000..f63ee5d68 --- /dev/null +++ b/core/src/main/java/org/apache/oozie/servlet/AuthFilter.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.oozie.servlet; + +import com.cloudera.alfredo.server.AuthenticationFilter; +import org.apache.hadoop.conf.Configuration; +import org.apache.oozie.service.Services; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * Authentication filter that extends Alfredo AuthenticationFilter to override + * the configuration loading. + */ +public class AuthFilter extends AuthenticationFilter { + private static final String OOZIE_PREFIX = "oozie.authentication."; + + private HttpServlet optionsServlet; + + /** + * Initialize the filter. + * + * @param filterConfig filter configuration. + * @throws ServletException thrown if the filter could not be initialized. + */ + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + optionsServlet = new HttpServlet() {}; + optionsServlet.init(); + } + + /** + * Destroy the filter. + */ + @Override + public void destroy() { + optionsServlet.destroy(); + super.destroy(); + } + + /** + * Returns the configuration from Oozie configuration to be used by the authentication filter. + *

+ * All properties from Oozie configuration which name starts with {@link #OOZIE_PREFIX} will + * be returned. The keys of the returned properties are trimmed from the {@link #OOZIE_PREFIX} + * prefix, for example the Oozie configuration property name 'oozie.authentication.type' will + * be just 'type'. + * + * @param configPrefix configuration prefix, this parameter is ignored by this implementation. + * @param filterConfig filter configuration, this parameter is ignored by this implementation. + * @return all Oozie configuration properties prefixed with {@link #OOZIE_PREFIX}, without the + * prefix. + */ + @Override + protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) { + Properties props = new Properties(); + Configuration conf = Services.get().getConf(); + + //setting the cookie path to root '/' so it is used for all resources. + props.setProperty(AuthenticationFilter.COOKIE_PATH, "/"); + + for (Map.Entry entry : conf) { + String name = entry.getKey(); + if (name.startsWith(OOZIE_PREFIX)) { + String value = conf.get(name); + name = name.substring(OOZIE_PREFIX.length()); + props.setProperty(name, value); + } + } + + return props; + } + + /** + * Enforces authentication using Alfredo AuthenticationFilter. + *

+ * This method is overriden to respond to HTTP OPTIONS requests for authenticated calls, regardless + * of the target servlet supporting OPTIONS or not and to inject the authenticated user name as + * request attribute for Oozie to retrieve the user id. + * + * @param request http request. + * @param response http response. + * @param filterChain filter chain. + * @throws IOException thrown if an IO error occurs. + * @throws ServletException thrown if a servlet error occurs. + */ + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + + FilterChain filterChainWrapper = new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + if (httpRequest.getMethod().equals("OPTIONS")) { + optionsServlet.service(request, response); + } + else { + httpRequest.setAttribute(JsonRestServlet.USER_NAME, httpRequest.getRemoteUser()); + filterChain.doFilter(servletRequest, servletResponse); + } + } + }; + + super.doFilter(request, response, filterChainWrapper); + } + +} diff --git a/core/src/main/resources/oozie-default.xml b/core/src/main/resources/oozie-default.xml index 6d52d9d59..1baa9b148 100644 --- a/core/src/main/resources/oozie-default.xml +++ b/core/src/main/resources/oozie-default.xml @@ -1197,4 +1197,71 @@ + + + + oozie.authentication.type + simple + + Defines authentication used for Oozie HTTP endpoint. + Supported values are: simple | kerberos | #AUTHENTICATION_HANDLER_CLASSNAME# + + + + + oozie.authentication.token.validity + 36000 + + Indicates how long (in seconds) an authentication token is valid before it has + to be renewed. + + + + + oozie.authentication.signature.secret + oozie + + The signature secret for signing the authentication tokens. + If not set a random secret is generated at startup time. + In order to authentiation to work correctly across multiple hosts + the secret must be the same across al the hosts. + + + + + oozie.authentication.cookie.domain + + + The domain to use for the HTTP cookie that stores the authentication token. + In order to authentiation to work correctly across multiple hosts + the domain must be correctly set. + + + + + oozie.authentication.simple.anonymous.allowed + true + + Indicates if anonymous requests are allowed when using 'simple' authentication. + + + + + oozie.authentication.kerberos.principal + HTTP/localhost@${local.realm} + + Indicates the Kerberos principal to be used for HTTP endpoint. + The principal MUST start with 'HTTP/' as per Kerberos HTTP SPNEGO specification. + + + + + oozie.authentication.kerberos.keytab + ${oozie.service.HadoopAccessorService.keytab.file} + + Location of the keytab file with the credentials for the principal. + Referring to the same keytab file Oozie uses for its Kerberos credentials for Hadoop. + + + diff --git a/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java b/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java new file mode 100644 index 000000000..071097c4a --- /dev/null +++ b/core/src/test/java/org/apache/oozie/servlet/TestAuthFilterAuthOozieClient.java @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.oozie.servlet; + +import com.cloudera.alfredo.client.AuthenticatedURL; +import com.cloudera.alfredo.client.AuthenticationException; +import com.cloudera.alfredo.client.PseudoAuthenticator; +import org.apache.oozie.cli.AuthOozieCLI; +import org.apache.oozie.cli.OozieCLI; +import org.apache.oozie.client.AuthOozieClient; +import org.apache.oozie.client.HeaderTestingVersionServlet; +import org.apache.oozie.client.XOozieClient; +import org.apache.oozie.service.ForTestAuthorizationService; +import org.apache.oozie.service.ForTestWorkflowStoreService; +import org.apache.oozie.service.Services; +import org.apache.oozie.test.EmbeddedServletContainer; +import org.apache.oozie.test.XTestCase; +import org.apache.oozie.util.IOUtils; + +import java.io.FileReader; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * + */ +public class TestAuthFilterAuthOozieClient extends XTestCase { + private EmbeddedServletContainer container; + + protected String getContextURL() { + return container.getContextURL(); + } + + protected URL createURL(String servletPath, String resource, Map parameters) throws Exception { + StringBuilder sb = new StringBuilder(); + sb.append(container.getServletURL(servletPath)); + if (resource != null && resource.length() > 0) { + sb.append("/").append(resource); + } + if (parameters.size() > 0) { + String separator = "?"; + for (Map.Entry param : parameters.entrySet()) { + sb.append(separator).append(URLEncoder.encode(param.getKey(), "UTF-8")).append("=") + .append(URLEncoder.encode(param.getValue(), "UTF-8")); + separator = "&"; + } + } + return new URL(sb.toString()); + } + + protected void runTest(Callable assertions) throws Exception { + Services services = new Services(); + try { + services.init(); + Services.get().setService(ForTestAuthorizationService.class); + Services.get().setService(ForTestWorkflowStoreService.class); + Services.get().setService(MockDagEngineService.class); + Services.get().setService(MockCoordinatorEngineService.class); + container = new EmbeddedServletContainer("oozie"); + container.addServletEndpoint("/versions", HeaderTestingVersionServlet.class); + String version = "/v" + XOozieClient.WS_PROTOCOL_VERSION; + container.addServletEndpoint(version + "/admin/*", V1AdminServlet.class); + container.addFilter("/*", AuthFilter.class); + container.start(); + assertions.call(); + } + finally { + if (container != null) { + container.stop(); + } + services.destroy(); + container = null; + } + } + + public static class Authenticator4Test extends PseudoAuthenticator { + + private static boolean USED = false; + + @Override + public void authenticate(URL url, AuthenticatedURL.Token token) throws IOException, AuthenticationException { + USED = true; + super.authenticate(url, token); + } + } + + public void testNonAuthUrlWithAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "true"); + runTest(new Callable() { + public Void call() throws Exception { + URL url = createURL("/versions", null, new HashMap()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + return null; + } + }); + } + + public void testNonAuthUrlWithoutAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + runTest(new Callable() { + public Void call() throws Exception { + URL url = createURL("/versions", null, new HashMap()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); + return null; + } + }); + } + + public void testNonAuthClientWithAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "true"); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new OozieCLI().run(args)); + return null; + } + }); + } + + public void testNonAuthClientWithoutAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertNotSame(0, new OozieCLI().run(args)); + return null; + } + }); + } + + public void testAuthClientWithAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "true"); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + } + + public void testAuthClientWithoutAnonymous() throws Exception { + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + } + + public void testAuthClientWithCustomAuthenticator() throws Exception { + setSystemProperty("authenticator.class", Authenticator4Test.class.getName()); + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + Authenticator4Test.USED = false; + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + assertTrue(Authenticator4Test.USED); + } + + + public void testAuthClientAuthTokenCache() throws Exception { + //not using cache + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + AuthOozieClient.AUTH_TOKEN_CACHE_FILE.delete(); + assertFalse(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + assertFalse(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + + //using cache + setSystemProperty("oozie.auth.token.cache", "true"); + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + setSystemProperty("oozie.authentication.signature.secret", "secret"); + AuthOozieClient.AUTH_TOKEN_CACHE_FILE.delete(); + assertFalse(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + assertTrue(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + String currentCache = IOUtils.getReaderAsString(new FileReader(AuthOozieClient.AUTH_TOKEN_CACHE_FILE), -1); + + //re-using cache + setSystemProperty("oozie.auth.token.cache", "true"); + setSystemProperty("oozie.authentication.simple.anonymous.allowed", "false"); + setSystemProperty("oozie.authentication.signature.secret", "secret"); + runTest(new Callable() { + public Void call() throws Exception { + String oozieUrl = getContextURL(); + String[] args = new String[]{"admin", "-status", "-oozie", oozieUrl}; + assertEquals(0, new AuthOozieCLI().run(args)); + return null; + } + }); + assertTrue(AuthOozieClient.AUTH_TOKEN_CACHE_FILE.exists()); + String newCache = IOUtils.getReaderAsString(new FileReader(AuthOozieClient.AUTH_TOKEN_CACHE_FILE), -1); + assertEquals(currentCache, newCache); + } + +} diff --git a/docs/src/site/twiki/AG_Install.twiki b/docs/src/site/twiki/AG_Install.twiki index 0f53924e8..28ea3dd93 100644 --- a/docs/src/site/twiki/AG_Install.twiki +++ b/docs/src/site/twiki/AG_Install.twiki @@ -160,11 +160,66 @@ Oozie logs in 4 different files: The embedded Tomcat and embedded Derby log files are also written to Oozie's =logs/= directory. ----+++ Oozie Authentication Configuration +---+++ Oozie User Authentication Configuration + +*IMPORTANT:* This feature is only available in Cloudera's distribution of Oozie (CDH3b4 onwards). + +Oozie supports Kerberos HTTP SPNEGO authentication, pseudo/simple authentication and anonymous access +for client connections. + +Anonymous access (*default*) does not require the user to authenticate and the user ID is obtained from +the job properties on job submission operations, other operations are anonymous. + +Pseudo/simple authentication requires the user to specify the user name on the request, this must be done +using the additional query string parameter =user.name=. + +Kerberos HTTP SPNEGO authentication requires the user to perform a Kerberos HTTP SPNEGO authentication sequence. + +If Pseudo/simple or Kerberos HTTP SPNEGO authentication mechanisms are used, Oozie will return the user an +authentication token HTTP Cookie that can be used in later requests as identy proof. + +Oozie uses [[http://www.github.com/cloudera/alfredo][Alfredo, Java HTTP SPENGO]] library for authentication. +This library can be extended to support other authentication mechanisms. + +Oozie user authentication is configured using the following configuration properties (default values shown): + + + oozie.authentication.type=simple + oozie.authentication.token.validity=36000 + oozie.authentication.signature.secret= + oozie.authentication.cookie.domain= + oozie.authentication.simple.anonymous.allowed=true + oozie.authentication.kerberos.principal=HTTP/localhost@${local.realm} + oozie.authentication.kerberos.keytab=${oozie.service.HadoopAccessorService.keytab.file} + + +The =type= defines authentication used for Oozie HTTP endpoint, the supported values are: +simple | kerberos | #AUTHENTICATION_HANDLER_CLASSNAME#. + +The =token.validity= indicates how long (in seconds) an authentication token is valid before it has +to be renewed. + +The =signature.secret= is the signature secret for signing the authentication tokens. If not set a random +secret is generated at startup time. + +The =oozie.authentication.cookie.domain= The domain to use for the HTTP cookie that stores the +authentication token. In order to authentiation to work correctly across all Hadoop nodes web-consoles +the domain must be correctly set. + +The =simple.anonymous.allowed= indicates if anonymous requests are allowed. This setting is meaningful +only when using 'simple' authentication. + +The =kerberos.principal= indicates the Kerberos principal to be used for HTTP endpoint. +The principal MUST start with 'HTTP/' as per Kerberos HTTP SPNEGO specification. + +The =kerberos.keytab= indicates the location of the keytab file with the credentials for the principal. +It should be the same keytab file Oozie uses for its Kerberos credentials for Hadoop. + +---+++ Oozie Hadoop Authentication Configuration Oozie can work with Hadoop 20 with Security distribution which supports Kerberos authentication. -Oozie authentication is configured using the following configuration properties (default values shown): +Oozie Hadoop authentication is configured using the following configuration properties (default values shown): oozie.service.HadoopAccessorService.kerberos.enabled=false diff --git a/docs/src/site/twiki/DG_CommandLineTool.twiki b/docs/src/site/twiki/DG_CommandLineTool.twiki index 6e169a6b2..ede377101 100644 --- a/docs/src/site/twiki/DG_CommandLineTool.twiki +++ b/docs/src/site/twiki/DG_CommandLineTool.twiki @@ -21,9 +21,9 @@ usage: custom headers for Oozie web services can be specified using '-Dheader:NAME=VALUE' oozie help : display usage - +. oozie version : show client version - +. oozie job : job operations -action coordinator rerun on action ids (requires -rerun) -change change a coordinator job @@ -52,7 +52,7 @@ usage: -value new endtime/concurrency/pausetime value for changing a coordinator job -verbose verbose mode - +. oozie jobs : jobs status -filter user=;name=;group=;status=;... -jobtype job type ('Supported in Oozie-2.0 or later versions ONLY - @@ -62,7 +62,7 @@ usage: -offset jobs offset (default '1') -oozie Oozie URL -verbose verbose mode - +. oozie admin : admin operations -oozie Oozie URL -queuedump show Oozie server queue elements @@ -70,14 +70,14 @@ usage: -systemmode Supported in Oozie-2.0 or later versions ONLY. Change oozie system mode [NORMAL|NOWEBSERVICE|SAFEMODE] -version show Oozie server build version - +. oozie validate : validate a workflow XML file - +. oozie sla : sla operations (Supported in Oozie-2.0 or later) -len number of results (default '100') -offset start offset (default '0') -oozie Oozie URL - +. oozie pig -X : submit a pig job, everything after '-X' are pass-through parameters to pig -config job configuration file '.properties' -D set/override value for given property @@ -87,6 +87,33 @@ usage: ---++ Common CLI Options +---+++ Authentication + +*IMPORTANT:* This feature is only available in Cloudera's distribution of Oozie (CDH3b3 onwards). + +The =oozie= CLI automatically perform authentication i the Oozie server requests it. By default it supports both +pseudo/simple authentication and Kerberos HTTP SPNEGO authentication. + +For pseudo/simple authentication the =oozie= CLI uses the user name of the current OS user. + +For Kerberos HTTP SPNEGO authentication the =oozie= CLI uses the default principal for the OS Kerberos cache +(normally the principal that did =kinit=). + +Oozie uses [[http://www.github.com/cloudera/alfredo][Alfredo, Java HTTP SPENGO]] library for authentication. +This library can be extended to support other authentication mechanisms. + +To disable authentication altogether the =-Doozie.client.class= option must be set with no value. This is required +for the =oozie= CLI to work with older versions of Oozie server. + +Once authentication is performed successfully the received authentication token is cached in the user home directory +in the =.oozie-auth-token= file with owner-only permissions. Subsequent requests reuse the cached token while valid. + +The use of the cache file can be disabled by invoking the =oozie= CLI with the =-Doozie.auth.token.cache=false= +option. + +To use an custom authentication mechanism, an Alfredo =Authenticator= implementation must be specified with the + =-Dauthenticator.class= = =CLASS= option. + ---+++ Oozie URL All =oozie= CLI sub-commands expect the -oozie OOZIE_URL option indicating the URL of the Oozie system @@ -257,7 +284,6 @@ After the command is executed the rerun coordiator action will be in =WAITING= s Refer to the [[DG_CoordinatorRerun][Rerunning Coordinator Actions]] for details on rerun. - ---+++ Checking the Status of a Workflow Job or Coordinator Job or Coordinator Action Example: diff --git a/pom.xml b/pom.xml index 919fcc70f..4cb311df5 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,13 @@ true + + cdh.releases + https://repository.cloudera.com/content/repositories/releases + + false + + @@ -220,7 +227,7 @@ commons-codec commons-codec - 1.3 + 1.4 @@ -425,7 +432,13 @@ 10.6.1.0 compile - + + + com.cloudera.alfredo + alfredo + 0.1.4 + + diff --git a/release-log.txt b/release-log.txt index fe2e273df..e7dc5780b 100644 --- a/release-log.txt +++ b/release-log.txt @@ -1,5 +1,6 @@ -- Oozie 3.0.0 release +GH-0035 Add support for Kerberos HTTP SPNEGO authentication (using Alfredo) GH-0542 Refactor CoordSuspendXCommand & CoordResumeXCommand to TransitionXCommand based GH-0543 Refactor CoordRerunXCommand to TransitionXCommand based GH-0530 Update workflow rerun twiki. diff --git a/webapp/pom.xml b/webapp/pom.xml index 1ae21fc26..427495ee3 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -67,14 +67,6 @@ javax.servlet jsp-api - - org.slf4j - slf4j-api - - - org.slf4j - slf4j-log4j12 - commons-logging commons-logging-api diff --git a/webapp/src/main/webapp/WEB-INF/web.xml b/webapp/src/main/webapp/WEB-INF/web.xml index 542960b4c..390427b92 100644 --- a/webapp/src/main/webapp/WEB-INF/web.xml +++ b/webapp/src/main/webapp/WEB-INF/web.xml @@ -143,4 +143,64 @@ index.html + + authenticationfilter + org.apache.oozie.servlet.AuthFilter + + + + authenticationfilter + /versions/* + + + + authenticationfilter + /v0/admin/* + + + + authenticationfilter + /v1/admin/* + + + + authenticationfilter + /v0/jobs + + + + authenticationfilter + /v1/jobs + + + + authenticationfilter + /v0/job/* + + + + authenticationfilter + /v1/job/* + + + + authenticationfilter + /index.html + + + + authenticationfilter + *.js + + + + authenticationfilter + /ext-2.2/* + + + + authenticationfilter + /docs/* + +