diff --git a/hadoop-common-project/hadoop-auth/pom.xml b/hadoop-common-project/hadoop-auth/pom.xml index 564518c540..5f7d77434b 100644 --- a/hadoop-common-project/hadoop-auth/pom.xml +++ b/hadoop-common-project/hadoop-auth/pom.xml @@ -130,6 +130,19 @@ + + org.apache.zookeeper + zookeeper + + + org.apache.curator + curator-framework + + + org.apache.curator + curator-test + test + diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java index 9330444c46..47cf54c606 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java @@ -22,6 +22,7 @@ import org.apache.hadoop.security.authentication.util.RandomSignerSecretProvider; import org.apache.hadoop.security.authentication.util.SignerSecretProvider; import org.apache.hadoop.security.authentication.util.StringSignerSecretProvider; +import org.apache.hadoop.security.authentication.util.ZKSignerSecretProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +43,7 @@ /** * The {@link AuthenticationFilter} enables protecting web application resources with different (pluggable) - * authentication mechanisms. + * authentication mechanisms and signer secret providers. *

* Out of the box it provides 2 authentication mechanisms: Pseudo and Kerberos SPNEGO. *

@@ -60,10 +61,13 @@ *

  • [#PREFIX#.]type: simple|kerberos|#CLASS#, 'simple' is short for the * {@link PseudoAuthenticationHandler}, 'kerberos' is short for {@link KerberosAuthenticationHandler}, otherwise * the full class name of the {@link AuthenticationHandler} must be specified.
  • - *
  • [#PREFIX#.]signature.secret: the secret used to sign the HTTP cookie value. The default value is a random - * value. Unless multiple webapp instances need to share the secret the random value is adequate.
  • - *
  • [#PREFIX#.]token.validity: time -in seconds- that the generated token is valid before a - * new authentication is triggered, default value is 3600 seconds.
  • + *
  • [#PREFIX#.]signature.secret: when signer.secret.provider is set to + * "string" or not specified, this is the value for the secret used to sign the + * HTTP cookie.
  • + *
  • [#PREFIX#.]token.validity: time -in seconds- that the generated token is + * valid before a new authentication is triggered, default value is + * 3600 seconds. This is also used for the rollover interval for + * the "random" and "zookeeper" SignerSecretProviders.
  • *
  • [#PREFIX#.]cookie.domain: domain to use for the HTTP cookie that stores the authentication token.
  • *
  • [#PREFIX#.]cookie.path: path to use for the HTTP cookie that stores the authentication token.
  • * @@ -72,6 +76,49 @@ * {@link AuthenticationFilter} will take all the properties that start with the prefix #PREFIX#, it will remove * the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do * not start with the prefix will not be passed to the authentication handler initialization. + *

    + * Out of the box it provides 3 signer secret provider implementations: + * "string", "random", and "zookeeper" + *

    + * Additional signer secret providers are supported via the + * {@link SignerSecretProvider} class. + *

    + * For the HTTP cookies mentioned above, the SignerSecretProvider is used to + * determine the secret to use for signing the cookies. Different + * implementations can have different behaviors. The "string" implementation + * simply uses the string set in the [#PREFIX#.]signature.secret property + * mentioned above. The "random" implementation uses a randomly generated + * secret that rolls over at the interval specified by the + * [#PREFIX#.]token.validity mentioned above. The "zookeeper" implementation + * is like the "random" one, except that it synchronizes the random secret + * and rollovers between multiple servers; it's meant for HA services. + *

    + * The relevant configuration properties are: + *

    + *

    + * The "zookeeper" implementation has additional configuration properties that + * must be specified; see {@link ZKSignerSecretProvider} for details. + *

    + * For subclasses of AuthenticationFilter that want additional control over the + * SignerSecretProvider, they can use the following attribute set in the + * ServletContext: + *

    */ @InterfaceAudience.Private @@ -112,20 +159,23 @@ public class AuthenticationFilter implements Filter { /** * Constant for the configuration property that indicates the name of the - * SignerSecretProvider class to use. If not specified, SIGNATURE_SECRET - * will be used or a random secret. + * SignerSecretProvider class to use. + * Possible values are: "string", "random", "zookeeper", or a classname. + * If not specified, the "string" implementation will be used with + * SIGNATURE_SECRET; and if that's not specified, the "random" implementation + * will be used. */ - public static final String SIGNER_SECRET_PROVIDER_CLASS = + public static final String SIGNER_SECRET_PROVIDER = "signer.secret.provider"; /** - * Constant for the attribute that can be used for providing a custom - * object that subclasses the SignerSecretProvider. Note that this should be - * set in the ServletContext and the class should already be initialized. - * If not specified, SIGNER_SECRET_PROVIDER_CLASS will be used. + * Constant for the ServletContext attribute that can be used for providing a + * custom implementation of the SignerSecretProvider. Note that the class + * should already be initialized. If not specified, SIGNER_SECRET_PROVIDER + * will be used. */ - public static final String SIGNATURE_PROVIDER_ATTRIBUTE = - "org.apache.hadoop.security.authentication.util.SignerSecretProvider"; + public static final String SIGNER_SECRET_PROVIDER_ATTRIBUTE = + "signer.secret.provider.object"; private Properties config; private Signer signer; @@ -138,7 +188,7 @@ public class AuthenticationFilter implements Filter { private String cookiePath; /** - * Initializes the authentication filter. + * Initializes the authentication filter and signer secret provider. *

    * It instantiates and initializes the specified {@link AuthenticationHandler}. *

    @@ -184,35 +234,19 @@ public void init(FilterConfig filterConfig) throws ServletException { validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000")) * 1000; //10 hours secretProvider = (SignerSecretProvider) filterConfig.getServletContext(). - getAttribute(SIGNATURE_PROVIDER_ATTRIBUTE); + getAttribute(SIGNER_SECRET_PROVIDER_ATTRIBUTE); if (secretProvider == null) { - String signerSecretProviderClassName = - config.getProperty(configPrefix + SIGNER_SECRET_PROVIDER_CLASS, null); - if (signerSecretProviderClassName == null) { - String signatureSecret = - config.getProperty(configPrefix + SIGNATURE_SECRET, null); - if (signatureSecret != null) { - secretProvider = new StringSignerSecretProvider(signatureSecret); - } else { - secretProvider = new RandomSignerSecretProvider(); - randomSecret = true; - } - } else { - try { - Class klass = Thread.currentThread().getContextClassLoader(). - loadClass(signerSecretProviderClassName); - secretProvider = (SignerSecretProvider) klass.newInstance(); - customSecretProvider = true; - } catch (ClassNotFoundException ex) { - throw new ServletException(ex); - } catch (InstantiationException ex) { - throw new ServletException(ex); - } catch (IllegalAccessException ex) { - throw new ServletException(ex); - } + Class providerClass + = getProviderClass(config); + try { + secretProvider = providerClass.newInstance(); + } catch (InstantiationException ex) { + throw new ServletException(ex); + } catch (IllegalAccessException ex) { + throw new ServletException(ex); } try { - secretProvider.init(config, validity); + secretProvider.init(config, filterConfig.getServletContext(), validity); } catch (Exception ex) { throw new ServletException(ex); } @@ -225,6 +259,42 @@ public void init(FilterConfig filterConfig) throws ServletException { cookiePath = config.getProperty(COOKIE_PATH, null); } + @SuppressWarnings("unchecked") + private Class getProviderClass(Properties config) + throws ServletException { + String providerClassName; + String signerSecretProviderName + = config.getProperty(SIGNER_SECRET_PROVIDER, null); + // fallback to old behavior + if (signerSecretProviderName == null) { + String signatureSecret = config.getProperty(SIGNATURE_SECRET, null); + if (signatureSecret != null) { + providerClassName = StringSignerSecretProvider.class.getName(); + } else { + providerClassName = RandomSignerSecretProvider.class.getName(); + randomSecret = true; + } + } else { + if ("random".equals(signerSecretProviderName)) { + providerClassName = RandomSignerSecretProvider.class.getName(); + randomSecret = true; + } else if ("string".equals(signerSecretProviderName)) { + providerClassName = StringSignerSecretProvider.class.getName(); + } else if ("zookeeper".equals(signerSecretProviderName)) { + providerClassName = ZKSignerSecretProvider.class.getName(); + } else { + providerClassName = signerSecretProviderName; + customSecretProvider = true; + } + } + try { + return (Class) Thread.currentThread(). + getContextClassLoader().loadClass(providerClassName); + } catch (ClassNotFoundException ex) { + throw new ServletException(ex); + } + } + /** * Returns the configuration properties of the {@link AuthenticationFilter} * without the prefix. The returned properties are the same that the diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RandomSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RandomSignerSecretProvider.java index 5491a8671b..29e5661cb0 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RandomSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RandomSignerSecretProvider.java @@ -13,12 +13,13 @@ */ package org.apache.hadoop.security.authentication.util; +import com.google.common.annotations.VisibleForTesting; import java.util.Random; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; /** - * A SignerSecretProvider that uses a random number as it's secret. It rolls + * A SignerSecretProvider that uses a random number as its secret. It rolls * the secret at a regular interval. */ @InterfaceStability.Unstable @@ -37,6 +38,7 @@ public RandomSignerSecretProvider() { * is meant for testing. * @param seed the seed for the random number generator */ + @VisibleForTesting public RandomSignerSecretProvider(long seed) { super(); rand = new Random(seed); diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java index ec6e601b4d..bdca3e4eb9 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java @@ -17,6 +17,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import javax.servlet.ServletContext; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.slf4j.Logger; @@ -57,12 +58,14 @@ public RolloverSignerSecretProvider() { * Initialize the SignerSecretProvider. It initializes the current secret * and starts the scheduler for the rollover to run at an interval of * tokenValidity. - * @param config filter configuration + * @param config configuration properties + * @param servletContext servlet context * @param tokenValidity The amount of time a token is valid for * @throws Exception */ @Override - public void init(Properties config, long tokenValidity) throws Exception { + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { initSecrets(generateNewSecret(), null); startScheduler(tokenValidity, tokenValidity); } diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java index a4d98d784f..2e0b985489 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java @@ -14,6 +14,7 @@ package org.apache.hadoop.security.authentication.util; import java.util.Properties; +import javax.servlet.ServletContext; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -30,13 +31,13 @@ public abstract class SignerSecretProvider { /** * Initialize the SignerSecretProvider - * @param config filter configuration + * @param config configuration properties + * @param servletContext servlet context * @param tokenValidity The amount of time a token is valid for * @throws Exception */ - public abstract void init(Properties config, long tokenValidity) - throws Exception; - + public abstract void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception; /** * Will be called on shutdown; subclasses should perform any cleanup here. */ diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java index 230059b645..7aaccd2914 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java @@ -14,8 +14,10 @@ package org.apache.hadoop.security.authentication.util; import java.util.Properties; +import javax.servlet.ServletContext; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; /** * A SignerSecretProvider that simply creates a secret based on a given String. @@ -27,14 +29,15 @@ public class StringSignerSecretProvider extends SignerSecretProvider { private byte[] secret; private byte[][] secrets; - public StringSignerSecretProvider(String secretStr) { - secret = secretStr.getBytes(); - secrets = new byte[][]{secret}; - } + public StringSignerSecretProvider() {} @Override - public void init(Properties config, long tokenValidity) throws Exception { - // do nothing + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + String signatureSecret = config.getProperty( + AuthenticationFilter.SIGNATURE_SECRET, null); + secret = signatureSecret.getBytes(); + secrets = new byte[][]{secret}; } @Override diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java new file mode 100644 index 0000000000..45d4d65307 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java @@ -0,0 +1,503 @@ +/** + * 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.hadoop.security.authentication.util; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.servlet.ServletContext; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.api.ACLProvider; +import org.apache.curator.framework.imps.DefaultACLProvider; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooDefs.Perms; +import org.apache.zookeeper.client.ZooKeeperSaslClient; +import org.apache.zookeeper.data.ACL; +import org.apache.zookeeper.data.Id; +import org.apache.zookeeper.data.Stat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A SignerSecretProvider that synchronizes a rolling random secret between + * multiple servers using ZooKeeper. + *

    + * It works by storing the secrets and next rollover time in a ZooKeeper znode. + * All ZKSignerSecretProviders looking at that znode will use those + * secrets and next rollover time to ensure they are synchronized. There is no + * "leader" -- any of the ZKSignerSecretProviders can choose the next secret; + * which one is indeterminate. Kerberos-based ACLs can also be enforced to + * prevent a malicious third-party from getting or setting the secrets. It uses + * its own CuratorFramework client for talking to ZooKeeper. If you want to use + * your own Curator client, you can pass it to ZKSignerSecretProvider; see + * {@link org.apache.hadoop.security.authentication.server.AuthenticationFilter} + * for more details. + *

    + * The supported configuration properties are: + *

    + * + * The following attribute in the ServletContext can also be set if desired: + *
  • signer.secret.provider.zookeeper.curator.client: A CuratorFramework + * client object can be passed here. If given, the "zookeeper" implementation + * will use this Curator client instead of creating its own, which is useful if + * you already have a Curator client or want more control over its + * configuration.
  • + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public class ZKSignerSecretProvider extends RolloverSignerSecretProvider { + + private static final String CONFIG_PREFIX = + "signer.secret.provider.zookeeper."; + + /** + * Constant for the property that specifies the ZooKeeper connection string. + */ + public static final String ZOOKEEPER_CONNECTION_STRING = + CONFIG_PREFIX + "connection.string"; + + /** + * Constant for the property that specifies the ZooKeeper path. + */ + public static final String ZOOKEEPER_PATH = CONFIG_PREFIX + "path"; + + /** + * Constant for the property that specifies the auth type to use. Supported + * values are "none" and "sasl". The default value is "none". + */ + public static final String ZOOKEEPER_AUTH_TYPE = CONFIG_PREFIX + "auth.type"; + + /** + * Constant for the property that specifies the Kerberos keytab file. + */ + public static final String ZOOKEEPER_KERBEROS_KEYTAB = + CONFIG_PREFIX + "kerberos.keytab"; + + /** + * Constant for the property that specifies the Kerberos principal. + */ + public static final String ZOOKEEPER_KERBEROS_PRINCIPAL = + CONFIG_PREFIX + "kerberos.principal"; + + /** + * Constant for the property that specifies whether or not the Curator client + * should disconnect from ZooKeeper on shutdown. The default is "true". Only + * set this to "false" if a custom Curator client is being provided and the + * disconnection is being handled elsewhere. + */ + public static final String DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN = + CONFIG_PREFIX + "disconnect.on.shutdown"; + + /** + * Constant for the ServletContext attribute that can be used for providing a + * custom CuratorFramework client. If set ZKSignerSecretProvider will use this + * Curator client instead of creating a new one. The providing class is + * responsible for creating and configuring the Curator client (including + * security and ACLs) in this case. + */ + public static final String + ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE = + CONFIG_PREFIX + "curator.client"; + + private static Logger LOG = LoggerFactory.getLogger( + ZKSignerSecretProvider.class); + private String path; + /** + * Stores the next secret that will be used after the current one rolls over. + * We do this to help with rollover performance by actually deciding the next + * secret at the previous rollover. This allows us to switch to the next + * secret very quickly. Afterwards, we have plenty of time to decide on the + * next secret. + */ + private volatile byte[] nextSecret; + private final Random rand; + /** + * Stores the current version of the znode. + */ + private int zkVersion; + /** + * Stores the next date that the rollover will occur. This is only used + * for allowing new servers joining later to synchronize their rollover + * with everyone else. + */ + private long nextRolloverDate; + private long tokenValidity; + private CuratorFramework client; + private boolean shouldDisconnect; + private static int INT_BYTES = Integer.SIZE / Byte.SIZE; + private static int LONG_BYTES = Long.SIZE / Byte.SIZE; + private static int DATA_VERSION = 0; + + public ZKSignerSecretProvider() { + super(); + rand = new Random(); + } + + /** + * This constructor lets you set the seed of the Random Number Generator and + * is meant for testing. + * @param seed the seed for the random number generator + */ + @VisibleForTesting + public ZKSignerSecretProvider(long seed) { + super(); + rand = new Random(seed); + } + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + Object curatorClientObj = servletContext.getAttribute( + ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE); + if (curatorClientObj != null + && curatorClientObj instanceof CuratorFramework) { + client = (CuratorFramework) curatorClientObj; + } else { + client = createCuratorClient(config); + } + this.tokenValidity = tokenValidity; + shouldDisconnect = Boolean.parseBoolean( + config.getProperty(DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN, "true")); + path = config.getProperty(ZOOKEEPER_PATH); + if (path == null) { + throw new IllegalArgumentException(ZOOKEEPER_PATH + + " must be specified"); + } + try { + nextRolloverDate = System.currentTimeMillis() + tokenValidity; + // everyone tries to do this, only one will succeed and only when the + // znode doesn't already exist. Everyone else will synchronize on the + // data from the znode + client.create().creatingParentsIfNeeded() + .forPath(path, generateZKData(generateRandomSecret(), + generateRandomSecret(), null)); + zkVersion = 0; + LOG.info("Creating secret znode"); + } catch (KeeperException.NodeExistsException nee) { + LOG.info("The secret znode already exists, retrieving data"); + } + // Synchronize on the data from the znode + // passing true tells it to parse out all the data for initing + pullFromZK(true); + long initialDelay = nextRolloverDate - System.currentTimeMillis(); + // If it's in the past, try to find the next interval that we should + // be using + if (initialDelay < 1l) { + int i = 1; + while (initialDelay < 1l) { + initialDelay = nextRolloverDate + tokenValidity * i + - System.currentTimeMillis(); + i++; + } + } + super.startScheduler(initialDelay, tokenValidity); + } + + /** + * Disconnects from ZooKeeper unless told not to. + */ + @Override + public void destroy() { + if (shouldDisconnect && client != null) { + client.close(); + } + super.destroy(); + } + + @Override + protected synchronized void rollSecret() { + super.rollSecret(); + // Try to push the information to ZooKeeper with a potential next secret. + nextRolloverDate += tokenValidity; + byte[][] secrets = super.getAllSecrets(); + pushToZK(generateRandomSecret(), secrets[0], secrets[1]); + // Pull info from ZooKeeper to get the decided next secret + // passing false tells it that we don't care about most of the data + pullFromZK(false); + } + + @Override + protected byte[] generateNewSecret() { + // We simply return nextSecret because it's already been decided on + return nextSecret; + } + + /** + * Pushes proposed data to ZooKeeper. If a different server pushes its data + * first, it gives up. + * @param newSecret The new secret to use + * @param currentSecret The current secret + * @param previousSecret The previous secret + */ + private synchronized void pushToZK(byte[] newSecret, byte[] currentSecret, + byte[] previousSecret) { + byte[] bytes = generateZKData(newSecret, currentSecret, previousSecret); + try { + client.setData().withVersion(zkVersion).forPath(path, bytes); + } catch (KeeperException.BadVersionException bve) { + LOG.debug("Unable to push to znode; another server already did it"); + } catch (Exception ex) { + LOG.error("An unexpected exception occured pushing data to ZooKeeper", + ex); + } + } + + /** + * Serialize the data to attempt to push into ZooKeeper. The format is this: + *

    + * [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, previousSecretLength, previousSecret, nextRolloverDate] + *

    + * Only previousSecret can be null, in which case the format looks like this: + *

    + * [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, 0, nextRolloverDate] + *

    + * @param newSecret The new secret to use + * @param currentSecret The current secret + * @param previousSecret The previous secret + * @return The serialized data for ZooKeeper + */ + private synchronized byte[] generateZKData(byte[] newSecret, + byte[] currentSecret, byte[] previousSecret) { + int newSecretLength = newSecret.length; + int currentSecretLength = currentSecret.length; + int previousSecretLength = 0; + if (previousSecret != null) { + previousSecretLength = previousSecret.length; + } + ByteBuffer bb = ByteBuffer.allocate(INT_BYTES + INT_BYTES + newSecretLength + + INT_BYTES + currentSecretLength + INT_BYTES + previousSecretLength + + LONG_BYTES); + bb.putInt(DATA_VERSION); + bb.putInt(newSecretLength); + bb.put(newSecret); + bb.putInt(currentSecretLength); + bb.put(currentSecret); + bb.putInt(previousSecretLength); + if (previousSecretLength > 0) { + bb.put(previousSecret); + } + bb.putLong(nextRolloverDate); + return bb.array(); + } + + /** + * Pulls data from ZooKeeper. If isInit is false, it will only parse the + * next secret and version. If isInit is true, it will also parse the current + * and previous secrets, and the next rollover date; it will also init the + * secrets. Hence, isInit should only be true on startup. + * @param isInit see description above + */ + private synchronized void pullFromZK(boolean isInit) { + try { + Stat stat = new Stat(); + byte[] bytes = client.getData().storingStatIn(stat).forPath(path); + ByteBuffer bb = ByteBuffer.wrap(bytes); + int dataVersion = bb.getInt(); + if (dataVersion > DATA_VERSION) { + throw new IllegalStateException("Cannot load data from ZooKeeper; it" + + "was written with a newer version"); + } + int nextSecretLength = bb.getInt(); + byte[] nextSecret = new byte[nextSecretLength]; + bb.get(nextSecret); + this.nextSecret = nextSecret; + zkVersion = stat.getVersion(); + if (isInit) { + int currentSecretLength = bb.getInt(); + byte[] currentSecret = new byte[currentSecretLength]; + bb.get(currentSecret); + int previousSecretLength = bb.getInt(); + byte[] previousSecret = null; + if (previousSecretLength > 0) { + previousSecret = new byte[previousSecretLength]; + bb.get(previousSecret); + } + super.initSecrets(currentSecret, previousSecret); + nextRolloverDate = bb.getLong(); + } + } catch (Exception ex) { + LOG.error("An unexpected exception occurred while pulling data from" + + "ZooKeeper", ex); + } + } + + private byte[] generateRandomSecret() { + return Long.toString(rand.nextLong()).getBytes(); + } + + /** + * This method creates the Curator client and connects to ZooKeeper. + * @param config configuration properties + * @return A Curator client + * @throws java.lang.Exception + */ + protected CuratorFramework createCuratorClient(Properties config) + throws Exception { + String connectionString = config.getProperty( + ZOOKEEPER_CONNECTION_STRING, "localhost:2181"); + + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + ACLProvider aclProvider; + String authType = config.getProperty(ZOOKEEPER_AUTH_TYPE, "none"); + if (authType.equals("sasl")) { + LOG.info("Connecting to ZooKeeper with SASL/Kerberos" + + "and using 'sasl' ACLs"); + String principal = setJaasConfiguration(config); + System.setProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, + "ZKSignerSecretProviderClient"); + System.setProperty("zookeeper.authProvider.1", + "org.apache.zookeeper.server.auth.SASLAuthenticationProvider"); + aclProvider = new SASLOwnerACLProvider(principal); + } else { // "none" + LOG.info("Connecting to ZooKeeper without authentication"); + aclProvider = new DefaultACLProvider(); // open to everyone + } + CuratorFramework cf = CuratorFrameworkFactory.builder() + .connectString(connectionString) + .retryPolicy(retryPolicy) + .aclProvider(aclProvider) + .build(); + cf.start(); + return cf; + } + + private String setJaasConfiguration(Properties config) throws Exception { + String keytabFile = config.getProperty(ZOOKEEPER_KERBEROS_KEYTAB).trim(); + if (keytabFile == null || keytabFile.length() == 0) { + throw new IllegalArgumentException(ZOOKEEPER_KERBEROS_KEYTAB + + " must be specified"); + } + String principal = config.getProperty(ZOOKEEPER_KERBEROS_PRINCIPAL) + .trim(); + if (principal == null || principal.length() == 0) { + throw new IllegalArgumentException(ZOOKEEPER_KERBEROS_PRINCIPAL + + " must be specified"); + } + + // This is equivalent to writing a jaas.conf file and setting the system + // property, "java.security.auth.login.config", to point to it + JaasConfiguration jConf = + new JaasConfiguration("Client", principal, keytabFile); + Configuration.setConfiguration(jConf); + return principal.split("[/@]")[0]; + } + + /** + * Simple implementation of an {@link ACLProvider} that simply returns an ACL + * that gives all permissions only to a single principal. + */ + private static class SASLOwnerACLProvider implements ACLProvider { + + private final List saslACL; + + private SASLOwnerACLProvider(String principal) { + this.saslACL = Collections.singletonList( + new ACL(Perms.ALL, new Id("sasl", principal))); + } + + @Override + public List getDefaultAcl() { + return saslACL; + } + + @Override + public List getAclForPath(String path) { + return saslACL; + } + } + + /** + * Creates a programmatic version of a jaas.conf file. This can be used + * instead of writing a jaas.conf file and setting the system property, + * "java.security.auth.login.config", to point to that file. It is meant to be + * used for connecting to ZooKeeper. + */ + @InterfaceAudience.Private + public static class JaasConfiguration extends Configuration { + + private static AppConfigurationEntry[] entry; + private String entryName; + + /** + * Add an entry to the jaas configuration with the passed in name, + * principal, and keytab. The other necessary options will be set for you. + * + * @param entryName The name of the entry (e.g. "Client") + * @param principal The principal of the user + * @param keytab The location of the keytab + */ + public JaasConfiguration(String entryName, String principal, String keytab) { + this.entryName = entryName; + Map options = new HashMap(); + options.put("keyTab", keytab); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("useTicketCache", "false"); + options.put("refreshKrb5Config", "true"); + String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG"); + if (jaasEnvVar != null && "true".equalsIgnoreCase(jaasEnvVar)) { + options.put("debug", "true"); + } + entry = new AppConfigurationEntry[]{ + new AppConfigurationEntry(getKrb5LoginModuleName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options)}; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return (entryName.equals(name)) ? entry : null; + } + + private String getKrb5LoginModuleName() { + String krb5LoginModuleName; + if (System.getProperty("java.vendor").contains("IBM")) { + krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule"; + } else { + krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule"; + } + return krb5LoginModuleName; + } + } +} diff --git a/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm b/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm index 6377393355..88248e5237 100644 --- a/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm +++ b/hadoop-common-project/hadoop-auth/src/site/apt/Configuration.apt.vm @@ -45,14 +45,14 @@ Configuration * <<<[PREFIX.]type>>>: the authentication type keyword (<<>> or <<>>) or a Authentication handler implementation. - * <<<[PREFIX.]signature.secret>>>: The secret to SHA-sign the generated - authentication tokens. If a secret is not provided a random secret is - generated at start up time. If using multiple web application instances - behind a load-balancer a secret must be set for the application to work - properly. + * <<<[PREFIX.]signature.secret>>>: When <<>> is set to + <<>> or not specified, this is the value for the secret used to sign + the HTTP cookie. * <<<[PREFIX.]token.validity>>>: The validity -in seconds- of the generated - authentication token. The default value is <<<3600>>> seconds. + authentication token. The default value is <<<3600>>> seconds. This is also + used for the rollover interval when <<>> is set to + <<>> or <<>>. * <<<[PREFIX.]cookie.domain>>>: domain to use for the HTTP cookie that stores the authentication token. @@ -60,6 +60,12 @@ Configuration * <<<[PREFIX.]cookie.path>>>: path to use for the HTTP cookie that stores the authentication token. + * <<>>: indicates the name of the SignerSecretProvider + class to use. Possible values are: <<>>, <<>>, + <<>>, or a classname. If not specified, the <<>> + implementation will be used; and failing that, the <<>> + implementation will be used. + ** Kerberos Configuration <>: A KDC must be configured and running. @@ -239,3 +245,133 @@ Configuration ... +---+ + +** SignerSecretProvider Configuration + + The SignerSecretProvider is used to provide more advanced behaviors for the + secret used for signing the HTTP Cookies. + + These are the relevant configuration properties: + + * <<>>: indicates the name of the + SignerSecretProvider class to use. Possible values are: "string", + "random", "zookeeper", or a classname. If not specified, the "string" + implementation will be used; and failing that, the "random" implementation + will be used. + + * <<<[PREFIX.]signature.secret>>>: When <<>> is set + to <<>> or not specified, this is the value for the secret used to + sign the HTTP cookie. + + * <<<[PREFIX.]token.validity>>>: The validity -in seconds- of the generated + authentication token. The default value is <<<3600>>> seconds. This is + also used for the rollover interval when <<>> is + set to <<>> or <<>>. + + The following configuration properties are specific to the <<>> + implementation: + + * <<>>: Indicates the + ZooKeeper connection string to connect with. + + * <<>>: Indicates the ZooKeeper path + to use for storing and retrieving the secrets. All servers + that need to coordinate their secret should point to the same path + + * <<>>: Indicates the auth type + to use. Supported values are <<>> and <<>>. The default + value is <<>>. + + * <<>>: Set this to the + path with the Kerberos keytab file. This is only required if using + Kerberos. + + * <<>>: Set this to the + Kerberos principal to use. This only required if using Kerberos. + + <>: + ++---+ + + ... + + + + + signer.secret.provider + string + + + signature.secret + my_secret + + + + ... + ++---+ + + <>: + ++---+ + + ... + + + + + signer.secret.provider + random + + + token.validity + 30 + + + + ... + ++---+ + + <>: + ++---+ + + ... + + + + + signer.secret.provider + zookeeper + + + token.validity + 30 + + + signer.secret.provider.zookeeper.connection.string + zoo1:2181,zoo2:2181,zoo3:2181 + + + signer.secret.provider.zookeeper.path + /myapp/secrets + + + signer.secret.provider.zookeeper.use.kerberos.acls + true + + + signer.secret.provider.zookeeper.kerberos.keytab + /tmp/auth.keytab + + + signer.secret.provider.zookeeper.kerberos.principal + HTTP/localhost@LOCALHOST + + + + ... + ++---+ + diff --git a/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm b/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm index 6051f8cbf2..bf85f7f41b 100644 --- a/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm +++ b/hadoop-common-project/hadoop-auth/src/site/apt/index.apt.vm @@ -44,6 +44,11 @@ Hadoop Auth, Java HTTP SPNEGO ${project.version} Subsequent HTTP client requests presenting the signed HTTP Cookie have access to the protected resources until the HTTP Cookie expires. + The secret used to sign the HTTP Cookie has multiple implementations that + provide different behaviors, including a hardcoded secret string, a rolling + randomly generated secret, and a rolling randomly generated secret + synchronized between multiple servers using ZooKeeper. + * User Documentation * {{{./Examples.html}Examples}} diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java index a9a5e8c738..5d93fcfa1c 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestAuthenticationFilter.java @@ -162,7 +162,8 @@ public void testInit() throws Exception { AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); Assert.assertEquals(PseudoAuthenticationHandler.class, filter.getAuthenticationHandler().getClass()); @@ -186,7 +187,8 @@ public void testInit() throws Exception { AuthenticationFilter.SIGNATURE_SECRET)).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); Assert.assertFalse(filter.isRandomSecret()); @@ -206,10 +208,11 @@ public void testInit() throws Exception { AuthenticationFilter.SIGNATURE_SECRET)).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn( + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)).thenReturn( new SignerSecretProvider() { @Override - public void init(Properties config, long tokenValidity) { + public void init(Properties config, ServletContext servletContext, + long tokenValidity) { } @Override public byte[] getCurrentSecret() { @@ -241,7 +244,8 @@ public byte[][] getAllSecrets() { AuthenticationFilter.COOKIE_PATH)).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); Assert.assertEquals(".foo.com", filter.getCookieDomain()); @@ -265,7 +269,8 @@ public byte[][] getAllSecrets() { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); Assert.assertTrue(DummyAuthenticationHandler.init); @@ -304,7 +309,8 @@ public void testInitCaseSensitivity() throws Exception { AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -330,7 +336,8 @@ public void testGetRequestURL() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -361,13 +368,20 @@ public void testGetToken() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -398,14 +412,21 @@ public void testGetTokenExpired() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); token.setExpires(System.currentTimeMillis() - TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -443,13 +464,20 @@ public void testGetTokenInvalidType() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); AuthenticationToken token = new AuthenticationToken("u", "p", "invalidtype"); token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -485,7 +513,8 @@ public void testDoFilterNotAuthenticated() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -538,7 +567,8 @@ private void _testDoFilterAuthentication(boolean withDomainPath, ".return", "expired.token")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); if (withDomainPath) { @@ -593,7 +623,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable { Mockito.verify(chain).doFilter(Mockito.any(ServletRequest.class), Mockito.any(ServletResponse.class)); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String value = signer.verifyAndExtract(v); AuthenticationToken token = AuthenticationToken.parse(value); assertThat(token.getExpires(), not(0L)); @@ -662,7 +698,8 @@ public void testDoFilterAuthenticated() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -671,7 +708,13 @@ public void testDoFilterAuthenticated() throws Exception { AuthenticationToken token = new AuthenticationToken("u", "p", "t"); token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -716,7 +759,8 @@ public void testDoFilterAuthenticationFailure() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -783,7 +827,8 @@ public void testDoFilterAuthenticatedExpired() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -792,7 +837,13 @@ public void testDoFilterAuthenticatedExpired() throws Exception { AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); token.setExpires(System.currentTimeMillis() - TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider(secret)); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, secret); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -854,7 +905,8 @@ public void testDoFilterAuthenticatedInvalidType() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -863,7 +915,13 @@ public void testDoFilterAuthenticatedInvalidType() throws Exception { AuthenticationToken token = new AuthenticationToken("u", "p", "invalidtype"); token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider(secret)); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, secret); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); @@ -893,7 +951,8 @@ public void testManagementOperation() throws Exception { "management.operation.return")).elements()); ServletContext context = Mockito.mock(ServletContext.class); Mockito.when(context.getAttribute( - AuthenticationFilter.SIGNATURE_PROVIDER_ATTRIBUTE)).thenReturn(null); + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); Mockito.when(config.getServletContext()).thenReturn(context); filter.init(config); @@ -914,7 +973,13 @@ public void testManagementOperation() throws Exception { AuthenticationToken token = new AuthenticationToken("u", "p", "t"); token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestJaasConfiguration.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestJaasConfiguration.java new file mode 100644 index 0000000000..2b70135800 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestJaasConfiguration.java @@ -0,0 +1,55 @@ +/** + * 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.hadoop.security.authentication.util; + +import java.util.Map; +import javax.security.auth.login.AppConfigurationEntry; +import org.junit.Assert; +import org.junit.Test; + +public class TestJaasConfiguration { + + // We won't test actually using it to authenticate because that gets messy and + // may conflict with other tests; but we can test that it otherwise behaves + // correctly + @Test + public void test() throws Exception { + String krb5LoginModuleName; + if (System.getProperty("java.vendor").contains("IBM")) { + krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule"; + } else { + krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule"; + } + + ZKSignerSecretProvider.JaasConfiguration jConf = + new ZKSignerSecretProvider.JaasConfiguration("foo", "foo/localhost", + "/some/location/foo.keytab"); + AppConfigurationEntry[] entries = jConf.getAppConfigurationEntry("bar"); + Assert.assertNull(entries); + entries = jConf.getAppConfigurationEntry("foo"); + Assert.assertEquals(1, entries.length); + AppConfigurationEntry entry = entries[0]; + Assert.assertEquals(AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + entry.getControlFlag()); + Assert.assertEquals(krb5LoginModuleName, entry.getLoginModuleName()); + Map options = entry.getOptions(); + Assert.assertEquals("/some/location/foo.keytab", options.get("keyTab")); + Assert.assertEquals("foo/localhost", options.get("principal")); + Assert.assertEquals("true", options.get("useKeyTab")); + Assert.assertEquals("true", options.get("storeKey")); + Assert.assertEquals("false", options.get("useTicketCache")); + Assert.assertEquals("true", options.get("refreshKrb5Config")); + Assert.assertEquals(6, options.size()); + } +} diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRandomSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRandomSignerSecretProvider.java index c3384ad03b..41d4967eac 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRandomSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRandomSignerSecretProvider.java @@ -31,7 +31,7 @@ public void testGetAndRollSecrets() throws Exception { RandomSignerSecretProvider secretProvider = new RandomSignerSecretProvider(seed); try { - secretProvider.init(null, rolloverFrequency); + secretProvider.init(null, null, rolloverFrequency); byte[] currentSecret = secretProvider.getCurrentSecret(); byte[][] allSecrets = secretProvider.getAllSecrets(); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRolloverSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRolloverSignerSecretProvider.java index 2a2986af9c..1e40c42326 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRolloverSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestRolloverSignerSecretProvider.java @@ -28,7 +28,7 @@ public void testGetAndRollSecrets() throws Exception { new TRolloverSignerSecretProvider( new byte[][]{secret1, secret2, secret3}); try { - secretProvider.init(null, rolloverFrequency); + secretProvider.init(null, null, rolloverFrequency); byte[] currentSecret = secretProvider.getCurrentSecret(); byte[][] allSecrets = secretProvider.getAllSecrets(); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSigner.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSigner.java index 1e2c960a92..c6a7710571 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSigner.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSigner.java @@ -14,6 +14,8 @@ package org.apache.hadoop.security.authentication.util; import java.util.Properties; +import javax.servlet.ServletContext; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; import org.junit.Assert; import org.junit.Test; @@ -21,7 +23,7 @@ public class TestSigner { @Test public void testNullAndEmptyString() throws Exception { - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + Signer signer = new Signer(createStringSignerSecretProvider()); try { signer.sign(null); Assert.fail(); @@ -42,7 +44,7 @@ public void testNullAndEmptyString() throws Exception { @Test public void testSignature() throws Exception { - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + Signer signer = new Signer(createStringSignerSecretProvider()); String s1 = signer.sign("ok"); String s2 = signer.sign("ok"); String s3 = signer.sign("wrong"); @@ -52,7 +54,7 @@ public void testSignature() throws Exception { @Test public void testVerify() throws Exception { - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + Signer signer = new Signer(createStringSignerSecretProvider()); String t = "test"; String s = signer.sign(t); String e = signer.verifyAndExtract(s); @@ -61,7 +63,7 @@ public void testVerify() throws Exception { @Test public void testInvalidSignedText() throws Exception { - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + Signer signer = new Signer(createStringSignerSecretProvider()); try { signer.verifyAndExtract("test"); Assert.fail(); @@ -74,7 +76,7 @@ public void testInvalidSignedText() throws Exception { @Test public void testTampering() throws Exception { - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + Signer signer = new Signer(createStringSignerSecretProvider()); String t = "test"; String s = signer.sign(t); s += "x"; @@ -88,6 +90,14 @@ public void testTampering() throws Exception { } } + private StringSignerSecretProvider createStringSignerSecretProvider() throws Exception { + StringSignerSecretProvider secretProvider = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty(AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, -1); + return secretProvider; + } + @Test public void testMultipleSecrets() throws Exception { TestSignerSecretProvider secretProvider = new TestSignerSecretProvider(); @@ -128,7 +138,8 @@ class TestSignerSecretProvider extends SignerSecretProvider { private byte[] previousSecret; @Override - public void init(Properties config, long tokenValidity) { + public void init(Properties config, ServletContext servletContext, + long tokenValidity) { } @Override diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestStringSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestStringSignerSecretProvider.java index c1170060ba..d8b044dcd2 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestStringSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestStringSignerSecretProvider.java @@ -13,6 +13,8 @@ */ package org.apache.hadoop.security.authentication.util; +import java.util.Properties; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; import org.junit.Assert; import org.junit.Test; @@ -22,8 +24,11 @@ public class TestStringSignerSecretProvider { public void testGetSecrets() throws Exception { String secretStr = "secret"; StringSignerSecretProvider secretProvider - = new StringSignerSecretProvider(secretStr); - secretProvider.init(null, -1); + = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, -1); byte[] secretBytes = secretStr.getBytes(); Assert.assertArrayEquals(secretBytes, secretProvider.getCurrentSecret()); byte[][] allSecrets = secretProvider.getAllSecrets(); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestZKSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestZKSignerSecretProvider.java new file mode 100644 index 0000000000..d7b6e17e11 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestZKSignerSecretProvider.java @@ -0,0 +1,270 @@ +/** + * 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.hadoop.security.authentication.util; + +import java.util.Arrays; +import java.util.Properties; +import java.util.Random; +import javax.servlet.ServletContext; +import org.apache.curator.test.TestingServer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class TestZKSignerSecretProvider { + + private TestingServer zkServer; + + @Before + public void setup() throws Exception { + zkServer = new TestingServer(); + } + + @After + public void teardown() throws Exception { + if (zkServer != null) { + zkServer.stop(); + zkServer.close(); + } + } + + @Test + // Test just one ZKSignerSecretProvider to verify that it works in the + // simplest case + public void testOne() throws Exception { + long rolloverFrequency = 15 * 1000; // rollover every 15 sec + // use the same seed so we can predict the RNG + long seed = System.currentTimeMillis(); + Random rand = new Random(seed); + byte[] secret2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secret1 = Long.toString(rand.nextLong()).getBytes(); + byte[] secret3 = Long.toString(rand.nextLong()).getBytes(); + ZKSignerSecretProvider secretProvider = new ZKSignerSecretProvider(seed); + Properties config = new Properties(); + config.setProperty( + ZKSignerSecretProvider.ZOOKEEPER_CONNECTION_STRING, + zkServer.getConnectString()); + config.setProperty(ZKSignerSecretProvider.ZOOKEEPER_PATH, + "/secret"); + try { + secretProvider.init(config, getDummyServletContext(), rolloverFrequency); + + byte[] currentSecret = secretProvider.getCurrentSecret(); + byte[][] allSecrets = secretProvider.getAllSecrets(); + Assert.assertArrayEquals(secret1, currentSecret); + Assert.assertEquals(2, allSecrets.length); + Assert.assertArrayEquals(secret1, allSecrets[0]); + Assert.assertNull(allSecrets[1]); + Thread.sleep((rolloverFrequency + 2000)); + + currentSecret = secretProvider.getCurrentSecret(); + allSecrets = secretProvider.getAllSecrets(); + Assert.assertArrayEquals(secret2, currentSecret); + Assert.assertEquals(2, allSecrets.length); + Assert.assertArrayEquals(secret2, allSecrets[0]); + Assert.assertArrayEquals(secret1, allSecrets[1]); + Thread.sleep((rolloverFrequency + 2000)); + + currentSecret = secretProvider.getCurrentSecret(); + allSecrets = secretProvider.getAllSecrets(); + Assert.assertArrayEquals(secret3, currentSecret); + Assert.assertEquals(2, allSecrets.length); + Assert.assertArrayEquals(secret3, allSecrets[0]); + Assert.assertArrayEquals(secret2, allSecrets[1]); + Thread.sleep((rolloverFrequency + 2000)); + } finally { + secretProvider.destroy(); + } + } + + @Test + public void testMultipleInit() throws Exception { + long rolloverFrequency = 15 * 1000; // rollover every 15 sec + // use the same seed so we can predict the RNG + long seedA = System.currentTimeMillis(); + Random rand = new Random(seedA); + byte[] secretA2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretA1 = Long.toString(rand.nextLong()).getBytes(); + // use the same seed so we can predict the RNG + long seedB = System.currentTimeMillis() + rand.nextLong(); + rand = new Random(seedB); + byte[] secretB2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretB1 = Long.toString(rand.nextLong()).getBytes(); + // use the same seed so we can predict the RNG + long seedC = System.currentTimeMillis() + rand.nextLong(); + rand = new Random(seedC); + byte[] secretC2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretC1 = Long.toString(rand.nextLong()).getBytes(); + ZKSignerSecretProvider secretProviderA = new ZKSignerSecretProvider(seedA); + ZKSignerSecretProvider secretProviderB = new ZKSignerSecretProvider(seedB); + ZKSignerSecretProvider secretProviderC = new ZKSignerSecretProvider(seedC); + Properties config = new Properties(); + config.setProperty( + ZKSignerSecretProvider.ZOOKEEPER_CONNECTION_STRING, + zkServer.getConnectString()); + config.setProperty(ZKSignerSecretProvider.ZOOKEEPER_PATH, + "/secret"); + try { + secretProviderA.init(config, getDummyServletContext(), rolloverFrequency); + secretProviderB.init(config, getDummyServletContext(), rolloverFrequency); + secretProviderC.init(config, getDummyServletContext(), rolloverFrequency); + + byte[] currentSecretA = secretProviderA.getCurrentSecret(); + byte[][] allSecretsA = secretProviderA.getAllSecrets(); + byte[] currentSecretB = secretProviderB.getCurrentSecret(); + byte[][] allSecretsB = secretProviderB.getAllSecrets(); + byte[] currentSecretC = secretProviderC.getCurrentSecret(); + byte[][] allSecretsC = secretProviderC.getAllSecrets(); + Assert.assertArrayEquals(currentSecretA, currentSecretB); + Assert.assertArrayEquals(currentSecretB, currentSecretC); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertEquals(2, allSecretsB.length); + Assert.assertEquals(2, allSecretsC.length); + Assert.assertArrayEquals(allSecretsA[0], allSecretsB[0]); + Assert.assertArrayEquals(allSecretsB[0], allSecretsC[0]); + Assert.assertNull(allSecretsA[1]); + Assert.assertNull(allSecretsB[1]); + Assert.assertNull(allSecretsC[1]); + char secretChosen = 'z'; + if (Arrays.equals(secretA1, currentSecretA)) { + Assert.assertArrayEquals(secretA1, allSecretsA[0]); + secretChosen = 'A'; + } else if (Arrays.equals(secretB1, currentSecretB)) { + Assert.assertArrayEquals(secretB1, allSecretsA[0]); + secretChosen = 'B'; + }else if (Arrays.equals(secretC1, currentSecretC)) { + Assert.assertArrayEquals(secretC1, allSecretsA[0]); + secretChosen = 'C'; + } else { + Assert.fail("It appears that they all agreed on the same secret, but " + + "not one of the secrets they were supposed to"); + } + Thread.sleep((rolloverFrequency + 2000)); + + currentSecretA = secretProviderA.getCurrentSecret(); + allSecretsA = secretProviderA.getAllSecrets(); + currentSecretB = secretProviderB.getCurrentSecret(); + allSecretsB = secretProviderB.getAllSecrets(); + currentSecretC = secretProviderC.getCurrentSecret(); + allSecretsC = secretProviderC.getAllSecrets(); + Assert.assertArrayEquals(currentSecretA, currentSecretB); + Assert.assertArrayEquals(currentSecretB, currentSecretC); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertEquals(2, allSecretsB.length); + Assert.assertEquals(2, allSecretsC.length); + Assert.assertArrayEquals(allSecretsA[0], allSecretsB[0]); + Assert.assertArrayEquals(allSecretsB[0], allSecretsC[0]); + Assert.assertArrayEquals(allSecretsA[1], allSecretsB[1]); + Assert.assertArrayEquals(allSecretsB[1], allSecretsC[1]); + // The second secret used is prechosen by whoever won the init; so it + // should match with whichever we saw before + if (secretChosen == 'A') { + Assert.assertArrayEquals(secretA2, currentSecretA); + } else if (secretChosen == 'B') { + Assert.assertArrayEquals(secretB2, currentSecretA); + } else if (secretChosen == 'C') { + Assert.assertArrayEquals(secretC2, currentSecretA); + } + } finally { + secretProviderC.destroy(); + secretProviderB.destroy(); + secretProviderA.destroy(); + } + } + + @Test + public void testMultipleUnsychnronized() throws Exception { + long rolloverFrequency = 15 * 1000; // rollover every 15 sec + // use the same seed so we can predict the RNG + long seedA = System.currentTimeMillis(); + Random rand = new Random(seedA); + byte[] secretA2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretA1 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretA3 = Long.toString(rand.nextLong()).getBytes(); + // use the same seed so we can predict the RNG + long seedB = System.currentTimeMillis() + rand.nextLong(); + rand = new Random(seedB); + byte[] secretB2 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretB1 = Long.toString(rand.nextLong()).getBytes(); + byte[] secretB3 = Long.toString(rand.nextLong()).getBytes(); + ZKSignerSecretProvider secretProviderA = new ZKSignerSecretProvider(seedA); + ZKSignerSecretProvider secretProviderB = new ZKSignerSecretProvider(seedB); + Properties config = new Properties(); + config.setProperty( + ZKSignerSecretProvider.ZOOKEEPER_CONNECTION_STRING, + zkServer.getConnectString()); + config.setProperty(ZKSignerSecretProvider.ZOOKEEPER_PATH, + "/secret"); + try { + secretProviderA.init(config, getDummyServletContext(), rolloverFrequency); + + byte[] currentSecretA = secretProviderA.getCurrentSecret(); + byte[][] allSecretsA = secretProviderA.getAllSecrets(); + Assert.assertArrayEquals(secretA1, currentSecretA); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertArrayEquals(secretA1, allSecretsA[0]); + Assert.assertNull(allSecretsA[1]); + Thread.sleep((rolloverFrequency + 2000)); + + currentSecretA = secretProviderA.getCurrentSecret(); + allSecretsA = secretProviderA.getAllSecrets(); + Assert.assertArrayEquals(secretA2, currentSecretA); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertArrayEquals(secretA2, allSecretsA[0]); + Assert.assertArrayEquals(secretA1, allSecretsA[1]); + Thread.sleep((rolloverFrequency / 5)); + + secretProviderB.init(config, getDummyServletContext(), rolloverFrequency); + + byte[] currentSecretB = secretProviderB.getCurrentSecret(); + byte[][] allSecretsB = secretProviderB.getAllSecrets(); + Assert.assertArrayEquals(secretA2, currentSecretB); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertArrayEquals(secretA2, allSecretsB[0]); + Assert.assertArrayEquals(secretA1, allSecretsB[1]); + Thread.sleep((rolloverFrequency)); + + currentSecretA = secretProviderA.getCurrentSecret(); + allSecretsA = secretProviderA.getAllSecrets(); + currentSecretB = secretProviderB.getCurrentSecret(); + allSecretsB = secretProviderB.getAllSecrets(); + Assert.assertArrayEquals(currentSecretA, currentSecretB); + Assert.assertEquals(2, allSecretsA.length); + Assert.assertEquals(2, allSecretsB.length); + Assert.assertArrayEquals(allSecretsA[0], allSecretsB[0]); + Assert.assertArrayEquals(allSecretsA[1], allSecretsB[1]); + if (Arrays.equals(secretA3, currentSecretA)) { + Assert.assertArrayEquals(secretA3, allSecretsA[0]); + } else if (Arrays.equals(secretB3, currentSecretB)) { + Assert.assertArrayEquals(secretB3, allSecretsA[0]); + } else { + Assert.fail("It appears that they all agreed on the same secret, but " + + "not one of the secrets they were supposed to"); + } + } finally { + secretProviderB.destroy(); + secretProviderA.destroy(); + } + } + + private ServletContext getDummyServletContext() { + ServletContext servletContext = Mockito.mock(ServletContext.class); + Mockito.when(servletContext.getAttribute(ZKSignerSecretProvider + .ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE)) + .thenReturn(null); + return servletContext; + } +} diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index 89bce4dd92..2d906f7f2b 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -520,6 +520,9 @@ Release 2.6.0 - UNRELEASED HADOOP-11091. Eliminate old configuration parameter names from s3a (David S. Wang via Colin Patrick McCabe) + HADOOP-10868. AuthenticationFilter should support externalizing the + secret for signing and provide rotation support. (rkanter via tucu) + OPTIMIZATIONS HADOOP-10838. Byte array native checksumming. (James Thomas via todd) diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java index c6c0d19d2a..763d168d19 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java @@ -66,6 +66,8 @@ import org.mortbay.jetty.webapp.WebAppContext; import com.google.common.collect.Maps; +import java.util.Properties; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.authentication.util.StringSignerSecretProvider; public class TestHttpFSServer extends HFSTestCase { @@ -685,7 +687,11 @@ public void testDelegationTokenOperations() throws Exception { new AuthenticationToken("u", "p", new KerberosDelegationTokenAuthenticationHandler().getType()); token.setExpires(System.currentTimeMillis() + 100000000); - Signer signer = new Signer(new StringSignerSecretProvider("secret")); + StringSignerSecretProvider secretProvider = new StringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty(AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, -1); + Signer signer = new Signer(secretProvider); String tokenSigned = signer.sign(token.toString()); url = new URL(TestJettyHelper.getJettyURL(), diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml index 502655f709..0f662a2049 100644 --- a/hadoop-project/pom.xml +++ b/hadoop-project/pom.xml @@ -849,6 +849,17 @@ xercesImpl 2.9.1 + + + org.apache.curator + curator-framework + 2.6.0 + + + org.apache.curator + curator-test + 2.6.0 +