Repository: accumulo Updated Branches: refs/heads/1.6.1-SNAPSHOT e2dc55027 -> 1ec33f1a5 refs/heads/master a050741af -> 85c1a496b
ACCUMULO-3045 Create a CredentialProviderToken PasswordToken where the password is retrieved from a Hadoop CredentialProvider. Extends the PasswordToken due to the reliance on it in the ZK-based authenticator and authorizor interfaces. Project: http://git-wip-us.apache.org/repos/asf/accumulo/repo Commit: http://git-wip-us.apache.org/repos/asf/accumulo/commit/1ec33f1a Tree: http://git-wip-us.apache.org/repos/asf/accumulo/tree/1ec33f1a Diff: http://git-wip-us.apache.org/repos/asf/accumulo/diff/1ec33f1a Branch: refs/heads/1.6.1-SNAPSHOT Commit: 1ec33f1a52c9166fab96b815fa89724b05737939 Parents: e2dc550 Author: Josh Elser <els...@apache.org> Authored: Mon Aug 4 21:14:09 2014 -0400 Committer: Josh Elser <els...@apache.org> Committed: Mon Aug 4 21:14:09 2014 -0400 ---------------------------------------------------------------------- .../tokens/CredentialProviderToken.java | 103 +++++++++++++++ .../client/security/tokens/PasswordToken.java | 8 +- .../conf/CredentialProviderFactoryShim.java | 106 ++++++++------- .../tokens/CredentialProviderTokenTest.java | 129 +++++++++++++++++++ .../conf/CredentialProviderFactoryShimTest.java | 8 ++ core/src/test/resources/passwords.jceks | Bin 0 -> 963 bytes 6 files changed, 306 insertions(+), 48 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java new file mode 100644 index 0000000..21e883b --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.accumulo.core.client.security.tokens; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.nio.CharBuffer; +import java.util.LinkedHashSet; +import java.util.Set; + +import jline.internal.Preconditions; + +import org.apache.accumulo.core.conf.CredentialProviderFactoryShim; +import org.apache.hadoop.conf.Configuration; + +/** + * An {@link AuthenticationToken} backed by a Hadoop CredentialProvider. + */ +public class CredentialProviderToken extends PasswordToken { + public static final String NAME_PROPERTY = "name", CREDENTIAL_PROVIDERS_PROPERTY = "credentialProviders"; + + public CredentialProviderToken() { + super(); + } + + public CredentialProviderToken(String name, String credentialProviders) throws IOException { + Preconditions.checkNotNull(name); + Preconditions.checkNotNull(credentialProviders); + + setWithCredentialProviders(name, credentialProviders); + } + + protected void setWithCredentialProviders(String name, String credentialProviders) throws IOException { + final Configuration conf = new Configuration(); + conf.set(CredentialProviderFactoryShim.CREDENTIAL_PROVIDER_PATH, credentialProviders); + + char[] password = CredentialProviderFactoryShim.getValueFromCredentialProvider(conf, name); + + if (null == password) { + throw new IOException("No password could be extracted from CredentialProvider(s) with " + name); + } + + setPassword(CharBuffer.wrap(password)); + } + + @Override + public void write(DataOutput out) throws IOException { + super.write(out); + } + + @Override + public void readFields(DataInput in) throws IOException { + super.readFields(in); + } + + @Override + public void init(Properties properties) { + char[] nameCharArray = properties.get(NAME_PROPERTY), credentialProvidersCharArray = properties.get(CREDENTIAL_PROVIDERS_PROPERTY); + if (null != nameCharArray && null != credentialProvidersCharArray) { + try { + this.setWithCredentialProviders(new String(nameCharArray), new String(credentialProvidersCharArray)); + } catch (IOException e) { + throw new IllegalArgumentException("Could not extract password from CredentialProvider", e); + } + + return; + } + + throw new IllegalArgumentException("Expected " + NAME_PROPERTY + " and " + CREDENTIAL_PROVIDERS_PROPERTY + " properties."); + } + + @Override + public Set<TokenProperty> getProperties() { + LinkedHashSet<TokenProperty> properties = new LinkedHashSet<TokenProperty>(); + // Neither name or CPs are sensitive + properties.add(new TokenProperty(NAME_PROPERTY, "Alias to extract from CredentialProvider", false)); + properties.add(new TokenProperty(CREDENTIAL_PROVIDERS_PROPERTY, "Comma separated list of URLs defining CredentialProvider(s)", false)); + return properties; + } + + @Override + public CredentialProviderToken clone() { + CredentialProviderToken clone = new CredentialProviderToken(); + clone.setPassword(this.getPassword()); + return clone; + } + +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java index 1474435..bd9feba 100644 --- a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java +++ b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java @@ -124,8 +124,12 @@ public class PasswordToken implements AuthenticationToken { throw new RuntimeException(e); } } - - private void setPassword(CharBuffer charBuffer) { + + protected void setPassword(byte[] password) { + this.password = Arrays.copyOf(password, password.length); + } + + protected void setPassword(CharBuffer charBuffer) { // encode() kicks back a C-string, which is not compatible with the old passwording system ByteBuffer bb = Constants.UTF8.encode(charBuffer); // create array using byter buffer length http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/main/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShim.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShim.java b/core/src/main/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShim.java index 1bf3ce6..a1fd8c7 100644 --- a/core/src/main/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShim.java +++ b/core/src/main/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShim.java @@ -37,26 +37,26 @@ import org.slf4j.LoggerFactory; */ public class CredentialProviderFactoryShim { private static final Logger log = LoggerFactory.getLogger(CredentialProviderFactoryShim.class); - - protected static final String HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME = "org.apache.hadoop.security.alias.JavaKeyStoreProvider$Factory"; - protected static final String HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME = "getProviders"; - - protected static final String HADOOP_CRED_PROVIDER_CLASS_NAME = "org.apache.hadoop.security.alias.CredentialProvider"; - protected static final String HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME = "getCredentialEntry"; - protected static final String HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME = "getAliases"; - - protected static final String HADOOP_CRED_ENTRY_CLASS_NAME = "org.apache.hadoop.security.alias.CredentialProvider$CredentialEntry"; - protected static final String HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME = "getCredential"; - + + public static final String HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME = "org.apache.hadoop.security.alias.JavaKeyStoreProvider$Factory"; + public static final String HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME = "getProviders"; + + public static final String HADOOP_CRED_PROVIDER_CLASS_NAME = "org.apache.hadoop.security.alias.CredentialProvider"; + public static final String HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME = "getCredentialEntry"; + public static final String HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME = "getAliases"; + + public static final String HADOOP_CRED_ENTRY_CLASS_NAME = "org.apache.hadoop.security.alias.CredentialProvider$CredentialEntry"; + public static final String HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME = "getCredential"; + public static final String CREDENTIAL_PROVIDER_PATH = "hadoop.security.credential.provider.path"; - + private static Object hadoopCredProviderFactory = null; private static Method getProvidersMethod = null; private static Method getAliasesMethod = null; private static Method getCredentialEntryMethod = null; private static Method getCredentialMethod = null; private static Boolean hadoopClassesAvailable = null; - + /** * Determine if we can load the necessary CredentialProvider classes. Only loaded the first time, so subsequent invocations of this method should return fast. * @@ -74,9 +74,9 @@ public class CredentialProviderFactoryShim { return false; } } - + hadoopClassesAvailable = false; - + // Load Hadoop CredentialProviderFactory Class<?> hadoopCredProviderFactoryClz = null; try { @@ -85,7 +85,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not load class {}", HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME, e); return false; } - + // Load Hadoop CredentialProviderFactory.getProviders(Configuration) try { getProvidersMethod = hadoopCredProviderFactoryClz.getMethod(HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME, Configuration.class); @@ -96,7 +96,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not find {} method on {}", HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME, HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME, e); return false; } - + // Instantiate Hadoop CredentialProviderFactory try { hadoopCredProviderFactory = hadoopCredProviderFactoryClz.newInstance(); @@ -107,7 +107,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not instantiate class {}", HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME, e); return false; } - + // Load Hadoop CredentialProvider Class<?> hadoopCredProviderClz = null; try { @@ -116,7 +116,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not load class {}", HADOOP_CRED_PROVIDER_CLASS_NAME, e); return false; } - + // Load Hadoop CredentialProvider.getCredentialEntry(String) try { getCredentialEntryMethod = hadoopCredProviderClz.getMethod(HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME, String.class); @@ -127,7 +127,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not find {} method on {}", HADOOP_CRED_PROVIDER_GET_CREDENTIAL_ENTRY_METHOD_NAME, HADOOP_CRED_PROVIDER_CLASS_NAME, e); return false; } - + // Load Hadoop CredentialProvider.getAliases() try { getAliasesMethod = hadoopCredProviderClz.getMethod(HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME); @@ -138,7 +138,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not find {} method on {}", HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME, HADOOP_CRED_PROVIDER_CLASS_NAME, e); return false; } - + // Load Hadoop CredentialEntry Class<?> hadoopCredentialEntryClz = null; try { @@ -147,7 +147,7 @@ public class CredentialProviderFactoryShim { log.trace("Could not load class {}", HADOOP_CRED_ENTRY_CLASS_NAME); return false; } - + // Load Hadoop CredentialEntry.getCredential() try { getCredentialMethod = hadoopCredentialEntryClz.getMethod(HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME); @@ -158,12 +158,12 @@ public class CredentialProviderFactoryShim { log.trace("Could not find {} method on {}", HADOOP_CRED_ENTRY_GET_CREDENTIAL_METHOD_NAME, HADOOP_CRED_ENTRY_CLASS_NAME, e); return false; } - + hadoopClassesAvailable = true; - + return true; } - + /** * Wrapper to fetch the configured {@code List<CredentialProvider>}s. * @@ -187,7 +187,7 @@ public class CredentialProviderFactoryShim { log.warn("Could not invoke {}.{}", HADOOP_CRED_PROVIDER_FACTORY_CLASS_NAME, HADOOP_CRED_PROVIDER_FACTORY_GET_PROVIDERS_METHOD_NAME, e); return null; } - + // Cast the Object to List<Object> (actually List<CredentialProvider>) try { return (List<Object>) providersObj; @@ -196,26 +196,26 @@ public class CredentialProviderFactoryShim { return null; } } - + protected static char[] getFromHadoopCredentialProvider(Configuration conf, String alias) { List<Object> providerObjList = getCredentialProviders(conf); - + if (null == providerObjList) { return null; } - + for (Object providerObj : providerObjList) { try { // Invoke CredentialProvider.getCredentialEntry(String) Object credEntryObj = getCredentialEntryMethod.invoke(providerObj, alias); - + if (null == credEntryObj) { continue; } - + // Then, CredentialEntry.getCredential() Object credential = getCredentialMethod.invoke(credEntryObj); - + return (char[]) credential; } catch (IllegalArgumentException e) { log.warn("Failed to get credential from {}", providerObj, e); @@ -228,28 +228,28 @@ public class CredentialProviderFactoryShim { continue; } } - + log.warn("Could not extract credential from providers"); - + return null; } - + @SuppressWarnings("unchecked") protected static List<String> getAliasesFromHadoopCredentialProvider(Configuration conf) { List<Object> providerObjList = getCredentialProviders(conf); - + if (null == providerObjList) { log.debug("Failed to get CredProviders"); return Collections.emptyList(); } - + ArrayList<String> aliases = new ArrayList<String>(); for (Object providerObj : providerObjList) { if (null != providerObj) { Object aliasesObj; try { aliasesObj = getAliasesMethod.invoke(providerObj); - + if (null != aliasesObj && aliasesObj instanceof List) { try { aliases.addAll((List<String>) aliasesObj); @@ -258,7 +258,7 @@ public class CredentialProviderFactoryShim { continue; } } - + } catch (IllegalArgumentException e) { log.warn("Failed to invoke {} on {}", HADOOP_CRED_PROVIDER_GET_ALIASES_METHOD_NAME, providerObj, e); continue; @@ -271,10 +271,24 @@ public class CredentialProviderFactoryShim { } } } - + return aliases; } - + + /** + * Create a Hadoop {@link Configuration} with the appropriate members to access CredentialProviders + * + * @param credentialProviders + * Comma-separated list of CredentialProvider URLs + * @return Configuration to be used for CredentialProvider + */ + public static Configuration getConfiguration(String credentialProviders) { + Preconditions.checkNotNull(credentialProviders); + final Configuration conf = new Configuration(); + conf.set(CredentialProviderFactoryShim.CREDENTIAL_PROVIDER_PATH, credentialProviders); + return conf; + } + /** * Attempt to extract the password from any configured CredentialsProviders for the given alias. If no providers or credential is found, null is returned. * @@ -289,15 +303,15 @@ public class CredentialProviderFactoryShim { public static char[] getValueFromCredentialProvider(Configuration conf, String alias) throws IOException { Preconditions.checkNotNull(conf); Preconditions.checkNotNull(alias); - + if (isHadoopCredentialProviderAvailable()) { log.trace("Hadoop CredentialProvider is available, attempting to extract value for {}", alias); return getFromHadoopCredentialProvider(conf, alias); } - + return null; } - + /** * Attempt to extract all aliases from any configured CredentialsProviders. * @@ -309,12 +323,12 @@ public class CredentialProviderFactoryShim { */ public static List<String> getKeys(Configuration conf) throws IOException { Preconditions.checkNotNull(conf); - + if (isHadoopCredentialProviderAvailable()) { log.trace("Hadoop CredentialProvider is available, attempting to extract all aliases"); return getAliasesFromHadoopCredentialProvider(conf); } - + return Collections.emptyList(); } } http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java b/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java new file mode 100644 index 0000000..17d2297 --- /dev/null +++ b/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.accumulo.core.client.security.tokens; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.apache.accumulo.core.Constants; +import org.apache.accumulo.core.client.security.tokens.AuthenticationToken.Properties; +import org.apache.accumulo.core.conf.CredentialProviderFactoryShim; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CredentialProviderTokenTest { + + private static boolean isCredentialProviderAvailable = false; + + // Keystore contains: {'root.password':'password', 'bob.password':'bob'} + private static String keystorePath; + + @BeforeClass + public static void setup() { + try { + Class.forName(CredentialProviderFactoryShim.HADOOP_CRED_PROVIDER_CLASS_NAME); + isCredentialProviderAvailable = true; + } catch (Exception e) { + isCredentialProviderAvailable = false; + } + + URL keystoreUrl = CredentialProviderTokenTest.class.getResource("/passwords.jceks"); + Assert.assertNotNull(keystoreUrl); + keystorePath = "jceks://file/" + new File(keystoreUrl.getFile()).getAbsolutePath(); + } + + @Test + public void testPasswordsFromCredentialProvider() throws Exception { + if (!isCredentialProviderAvailable) { + return; + } + + CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); + Assert.assertArrayEquals("password".getBytes(Constants.UTF8), token.getPassword()); + + token = new CredentialProviderToken("bob.password", keystorePath); + Assert.assertArrayEquals("bob".getBytes(Constants.UTF8), token.getPassword()); + } + + @Test + public void testEqualityAfterInit() throws Exception { + if (!isCredentialProviderAvailable) { + return; + } + + CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); + + CredentialProviderToken uninitializedToken = new CredentialProviderToken(); + Properties props = new Properties(); + props.put(CredentialProviderToken.NAME_PROPERTY, "root.password"); + props.put(CredentialProviderToken.CREDENTIAL_PROVIDERS_PROPERTY, keystorePath); + uninitializedToken.init(props); + + Assert.assertArrayEquals(token.getPassword(), uninitializedToken.getPassword()); + } + + @Test + public void testMissingClassesThrowsException() throws Exception { + if (isCredentialProviderAvailable) { + return; + } + + try { + new CredentialProviderToken("root.password", keystorePath); + Assert.fail("Should fail to create CredentialProviderToken when classes are not available"); + } catch (IOException e) { + // pass + } + } + + @Test + public void cloneReturnsCorrectObject() throws Exception { + if (!isCredentialProviderAvailable) { + return; + } + + CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); + CredentialProviderToken clone = token.clone(); + + Assert.assertEquals(token, clone); + Assert.assertArrayEquals(token.getPassword(), clone.getPassword()); + } + + @Test(expected = IllegalArgumentException.class) + public void missingProperties() throws Exception { + CredentialProviderToken token = new CredentialProviderToken(); + token.init(new Properties()); + } + + @Test(expected = IllegalArgumentException.class) + public void missingNameProperty() throws Exception { + CredentialProviderToken token = new CredentialProviderToken(); + Properties props = new Properties(); + props.put(CredentialProviderToken.NAME_PROPERTY, "root.password"); + token.init(props); + } + + @Test(expected = IllegalArgumentException.class) + public void missingProviderProperty() throws Exception { + CredentialProviderToken token = new CredentialProviderToken(); + Properties props = new Properties(); + props.put(CredentialProviderToken.CREDENTIAL_PROVIDERS_PROPERTY, keystorePath); + token.init(props); + } +} http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/test/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShimTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShimTest.java b/core/src/test/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShimTest.java index 42497a8..100e297 100644 --- a/core/src/test/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShimTest.java +++ b/core/src/test/java/org/apache/accumulo/core/conf/CredentialProviderFactoryShimTest.java @@ -146,4 +146,12 @@ public class CredentialProviderFactoryShimTest { Assert.assertNull(CredentialProviderFactoryShim.getValueFromCredentialProvider(conf, "key1")); } + + @Test + public void testConfigurationCreation() throws IOException { + final String path = "jceks://file/tmp/foo.jks"; + final Configuration actualConf = CredentialProviderFactoryShim.getConfiguration(path); + Assert.assertNotNull(actualConf); + Assert.assertEquals(path, actualConf.get(CredentialProviderFactoryShim.CREDENTIAL_PROVIDER_PATH)); + } } http://git-wip-us.apache.org/repos/asf/accumulo/blob/1ec33f1a/core/src/test/resources/passwords.jceks ---------------------------------------------------------------------- diff --git a/core/src/test/resources/passwords.jceks b/core/src/test/resources/passwords.jceks new file mode 100755 index 0000000..9bbb6bc Binary files /dev/null and b/core/src/test/resources/passwords.jceks differ