HADOOP-12548. Read s3a creds from a Credential Provider. Contributed by Larry McCay.

This commit is contained in:
cnauroth 2016-02-17 10:17:12 -08:00
parent 3a23dc683c
commit 76fab26c5c
3 changed files with 301 additions and 65 deletions

View File

@ -766,6 +766,9 @@ Release 2.8.0 - UNRELEASED
HADOOP-12426. Add Entry point for Kerberos health check HADOOP-12426. Add Entry point for Kerberos health check
(Steve Loughran via cnauroth) (Steve Loughran via cnauroth)
HADOOP-12548. Read s3a creds from a Credential Provider.
(Larry McCay via cnauroth)
IMPROVEMENTS IMPROVEMENTS
HADOOP-12458. Retries is typoed to spell Retires in parts of HADOOP-12458. Retries is typoed to spell Retires in parts of

View File

@ -106,23 +106,11 @@ public void initialize(URI name, Configuration conf) throws IOException {
workingDir = new Path("/user", System.getProperty("user.name")).makeQualified(this.uri, workingDir = new Path("/user", System.getProperty("user.name")).makeQualified(this.uri,
this.getWorkingDirectory()); this.getWorkingDirectory());
// Try to get our credentials or just connect anonymously AWSAccessKeys creds = getAWSAccessKeys(name, conf);
String accessKey = conf.get(ACCESS_KEY, null);
String secretKey = conf.get(SECRET_KEY, null);
String userInfo = name.getUserInfo();
if (userInfo != null) {
int index = userInfo.indexOf(':');
if (index != -1) {
accessKey = userInfo.substring(0, index);
secretKey = userInfo.substring(index + 1);
} else {
accessKey = userInfo;
}
}
AWSCredentialsProviderChain credentials = new AWSCredentialsProviderChain( AWSCredentialsProviderChain credentials = new AWSCredentialsProviderChain(
new BasicAWSCredentialsProvider(accessKey, secretKey), new BasicAWSCredentialsProvider(
creds.getAccessKey(), creds.getAccessSecret()),
new InstanceProfileCredentialsProvider(), new InstanceProfileCredentialsProvider(),
new AnonymousAWSCredentialsProvider() new AnonymousAWSCredentialsProvider()
); );
@ -146,6 +134,59 @@ public void initialize(URI name, Configuration conf) throws IOException {
awsConf.setSignerOverride(signerOverride); awsConf.setSignerOverride(signerOverride);
} }
initProxySupport(conf, awsConf, secureConnections);
initAmazonS3Client(conf, credentials, awsConf);
maxKeys = conf.getInt(MAX_PAGING_KEYS, DEFAULT_MAX_PAGING_KEYS);
partSize = conf.getLong(MULTIPART_SIZE, DEFAULT_MULTIPART_SIZE);
multiPartThreshold = conf.getLong(MIN_MULTIPART_THRESHOLD,
DEFAULT_MIN_MULTIPART_THRESHOLD);
enableMultiObjectsDelete = conf.getBoolean(ENABLE_MULTI_DELETE, true);
if (partSize < 5 * 1024 * 1024) {
LOG.error(MULTIPART_SIZE + " must be at least 5 MB");
partSize = 5 * 1024 * 1024;
}
if (multiPartThreshold < 5 * 1024 * 1024) {
LOG.error(MIN_MULTIPART_THRESHOLD + " must be at least 5 MB");
multiPartThreshold = 5 * 1024 * 1024;
}
int maxThreads = conf.getInt(MAX_THREADS, DEFAULT_MAX_THREADS);
if (maxThreads < 2) {
LOG.warn(MAX_THREADS + " must be at least 2: forcing to 2.");
maxThreads = 2;
}
int totalTasks = conf.getInt(MAX_TOTAL_TASKS, DEFAULT_MAX_TOTAL_TASKS);
if (totalTasks < 1) {
LOG.warn(MAX_TOTAL_TASKS + "must be at least 1: forcing to 1.");
totalTasks = 1;
}
long keepAliveTime = conf.getLong(KEEPALIVE_TIME, DEFAULT_KEEPALIVE_TIME);
threadPoolExecutor = new BlockingThreadPoolExecutorService(maxThreads,
maxThreads + totalTasks, keepAliveTime, TimeUnit.SECONDS,
"s3a-transfer-shared");
initTransferManager();
initCannedAcls(conf);
if (!s3.doesBucketExist(bucket)) {
throw new IOException("Bucket " + bucket + " does not exist");
}
initMultipartUploads(conf);
serverSideEncryptionAlgorithm = conf.get(SERVER_SIDE_ENCRYPTION_ALGORITHM);
setConf(conf);
}
void initProxySupport(Configuration conf, ClientConfiguration awsConf,
boolean secureConnections) throws IllegalArgumentException,
IllegalArgumentException {
String proxyHost = conf.getTrimmed(PROXY_HOST, ""); String proxyHost = conf.getTrimmed(PROXY_HOST, "");
int proxyPort = conf.getInt(PROXY_PORT, -1); int proxyPort = conf.getInt(PROXY_PORT, -1);
if (!proxyHost.isEmpty()) { if (!proxyHost.isEmpty()) {
@ -185,7 +226,11 @@ public void initialize(URI name, Configuration conf) throws IOException {
LOG.error(msg); LOG.error(msg);
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
}
private void initAmazonS3Client(Configuration conf,
AWSCredentialsProviderChain credentials, ClientConfiguration awsConf)
throws IllegalArgumentException {
s3 = new AmazonS3Client(credentials, awsConf); s3 = new AmazonS3Client(credentials, awsConf);
String endPoint = conf.getTrimmed(ENDPOINT,""); String endPoint = conf.getTrimmed(ENDPOINT,"");
if (!endPoint.isEmpty()) { if (!endPoint.isEmpty()) {
@ -197,56 +242,27 @@ public void initialize(URI name, Configuration conf) throws IOException {
throw new IllegalArgumentException(msg, e); throw new IllegalArgumentException(msg, e);
} }
} }
}
maxKeys = conf.getInt(MAX_PAGING_KEYS, DEFAULT_MAX_PAGING_KEYS); private void initTransferManager() {
partSize = conf.getLong(MULTIPART_SIZE, DEFAULT_MULTIPART_SIZE);
multiPartThreshold = conf.getLong(MIN_MULTIPART_THRESHOLD,
DEFAULT_MIN_MULTIPART_THRESHOLD);
enableMultiObjectsDelete = conf.getBoolean(ENABLE_MULTI_DELETE, true);
if (partSize < 5 * 1024 * 1024) {
LOG.error(MULTIPART_SIZE + " must be at least 5 MB");
partSize = 5 * 1024 * 1024;
}
if (multiPartThreshold < 5 * 1024 * 1024) {
LOG.error(MIN_MULTIPART_THRESHOLD + " must be at least 5 MB");
multiPartThreshold = 5 * 1024 * 1024;
}
int maxThreads = conf.getInt(MAX_THREADS, DEFAULT_MAX_THREADS);
if (maxThreads < 2) {
LOG.warn(MAX_THREADS + " must be at least 2: forcing to 2.");
maxThreads = 2;
}
int totalTasks = conf.getInt(MAX_TOTAL_TASKS, DEFAULT_MAX_TOTAL_TASKS);
if (totalTasks < 1) {
LOG.warn(MAX_TOTAL_TASKS + "must be at least 1: forcing to 1.");
totalTasks = 1;
}
long keepAliveTime = conf.getLong(KEEPALIVE_TIME, DEFAULT_KEEPALIVE_TIME);
threadPoolExecutor = new BlockingThreadPoolExecutorService(maxThreads,
maxThreads + totalTasks, keepAliveTime, TimeUnit.SECONDS,
"s3a-transfer-shared");
TransferManagerConfiguration transferConfiguration = new TransferManagerConfiguration(); TransferManagerConfiguration transferConfiguration = new TransferManagerConfiguration();
transferConfiguration.setMinimumUploadPartSize(partSize); transferConfiguration.setMinimumUploadPartSize(partSize);
transferConfiguration.setMultipartUploadThreshold(multiPartThreshold); transferConfiguration.setMultipartUploadThreshold(multiPartThreshold);
transfers = new TransferManager(s3, threadPoolExecutor); transfers = new TransferManager(s3, threadPoolExecutor);
transfers.setConfiguration(transferConfiguration); transfers.setConfiguration(transferConfiguration);
}
private void initCannedAcls(Configuration conf) {
String cannedACLName = conf.get(CANNED_ACL, DEFAULT_CANNED_ACL); String cannedACLName = conf.get(CANNED_ACL, DEFAULT_CANNED_ACL);
if (!cannedACLName.isEmpty()) { if (!cannedACLName.isEmpty()) {
cannedACL = CannedAccessControlList.valueOf(cannedACLName); cannedACL = CannedAccessControlList.valueOf(cannedACLName);
} else { } else {
cannedACL = null; cannedACL = null;
} }
}
if (!s3.doesBucketExist(bucket)) { private void initMultipartUploads(Configuration conf) {
throw new IOException("Bucket " + bucket + " does not exist");
}
boolean purgeExistingMultipart = conf.getBoolean(PURGE_EXISTING_MULTIPART, boolean purgeExistingMultipart = conf.getBoolean(PURGE_EXISTING_MULTIPART,
DEFAULT_PURGE_EXISTING_MULTIPART); DEFAULT_PURGE_EXISTING_MULTIPART);
long purgeExistingMultipartAge = conf.getLong(PURGE_EXISTING_MULTIPART_AGE, long purgeExistingMultipartAge = conf.getLong(PURGE_EXISTING_MULTIPART_AGE,
@ -257,10 +273,51 @@ public void initialize(URI name, Configuration conf) throws IOException {
transfers.abortMultipartUploads(bucket, purgeBefore); transfers.abortMultipartUploads(bucket, purgeBefore);
} }
}
serverSideEncryptionAlgorithm = conf.get(SERVER_SIDE_ENCRYPTION_ALGORITHM); /**
* Return the access key and secret for S3 API use.
setConf(conf); * Credentials may exist in configuration, within credential providers
* or indicated in the UserInfo of the name URI param.
* @param name the URI for which we need the access keys.
* @param conf the Configuration object to interogate for keys.
* @return AWSAccessKeys
*/
AWSAccessKeys getAWSAccessKeys(URI name, Configuration conf)
throws IOException {
String accessKey = null;
String secretKey = null;
String userInfo = name.getUserInfo();
if (userInfo != null) {
int index = userInfo.indexOf(':');
if (index != -1) {
accessKey = userInfo.substring(0, index);
secretKey = userInfo.substring(index + 1);
} else {
accessKey = userInfo;
}
}
if (accessKey == null) {
try {
final char[] key = conf.getPassword(ACCESS_KEY);
if (key != null) {
accessKey = (new String(key)).trim();
}
} catch(IOException ioe) {
throw new IOException("Cannot find AWS access key.", ioe);
}
}
if (secretKey == null) {
try {
final char[] pass = conf.getPassword(SECRET_KEY);
if (pass != null) {
secretKey = (new String(pass)).trim();
}
} catch(IOException ioe) {
throw new IOException("Cannot find AWS secret key.", ioe);
}
}
return new AWSAccessKeys(accessKey, secretKey);
} }
/** /**
@ -341,14 +398,14 @@ public FSDataInputStream open(Path f, int bufferSize)
* Create an FSDataOutputStream at the indicated Path with write-progress * Create an FSDataOutputStream at the indicated Path with write-progress
* reporting. * reporting.
* @param f the file name to open * @param f the file name to open
* @param permission * @param permission the permission to set.
* @param overwrite if a file with this name already exists, then if true, * @param overwrite if a file with this name already exists, then if true,
* the file will be overwritten, and if false an error will be thrown. * the file will be overwritten, and if false an error will be thrown.
* @param bufferSize the size of the buffer to be used. * @param bufferSize the size of the buffer to be used.
* @param replication required block replication for the file. * @param replication required block replication for the file.
* @param blockSize * @param blockSize the requested block size.
* @param progress * @param progress the progress reporter.
* @throws IOException * @throws IOException in the event of IO related errors.
* @see #setPermission(Path, FsPermission) * @see #setPermission(Path, FsPermission)
*/ */
@Override @Override
@ -377,7 +434,7 @@ public FSDataOutputStream create(Path f, FsPermission permission,
* @param f the existing file to be appended. * @param f the existing file to be appended.
* @param bufferSize the size of the buffer to be used. * @param bufferSize the size of the buffer to be used.
* @param progress for reporting progress if it is not null. * @param progress for reporting progress if it is not null.
* @throws IOException * @throws IOException indicating that append is not supported.
*/ */
public FSDataOutputStream append(Path f, int bufferSize, public FSDataOutputStream append(Path f, int bufferSize,
Progressable progress) throws IOException { Progressable progress) throws IOException {
@ -585,7 +642,7 @@ private void removeKeys(List<DeleteObjectsRequest.KeyVersion> keysToDelete,
* true, the directory is deleted else throws an exception. In * true, the directory is deleted else throws an exception. In
* case of a file the recursive can be set to either true or false. * case of a file the recursive can be set to either true or false.
* @return true if delete is successful else false. * @return true if delete is successful else false.
* @throws IOException * @throws IOException due to inability to delete a directory or file.
*/ */
public boolean delete(Path f, boolean recursive) throws IOException { public boolean delete(Path f, boolean recursive) throws IOException {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
@ -791,7 +848,7 @@ public FileStatus[] listStatus(Path f) throws FileNotFoundException,
* Set the current working directory for the given file system. All relative * Set the current working directory for the given file system. All relative
* paths will be resolved relative to it. * paths will be resolved relative to it.
* *
* @param new_dir * @param new_dir the current working directory.
*/ */
public void setWorkingDirectory(Path new_dir) { public void setWorkingDirectory(Path new_dir) {
workingDir = new_dir; workingDir = new_dir;
@ -1202,4 +1259,39 @@ private void printAmazonClientException(AmazonClientException ace) {
"such as not being able to access the network."); "such as not being able to access the network.");
LOG.info("Error Message: {}" + ace, ace); LOG.info("Error Message: {}" + ace, ace);
} }
/**
* This is a simple encapsulation of the
* S3 access key and secret.
*/
static class AWSAccessKeys {
private String accessKey = null;
private String accessSecret = null;
/**
* Constructor.
* @param key - AWS access key
* @param secret - AWS secret key
*/
public AWSAccessKeys(String key, String secret) {
accessKey = key;
accessSecret = secret;
}
/**
* Return the AWS access key.
* @return key
*/
public String getAccessKey() {
return accessKey;
}
/**
* Return the AWS secret key.
* @return secret
*/
public String getAccessSecret() {
return accessSecret;
}
}
} }

View File

@ -32,7 +32,21 @@
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.File;
import java.net.URI;
import java.io.IOException;
import org.apache.hadoop.security.ProviderUtils;
import org.apache.hadoop.security.alias.CredentialProvider;
import org.apache.hadoop.security.alias.CredentialProviderFactory;
import org.junit.rules.TemporaryFolder;
public class TestS3AConfiguration { public class TestS3AConfiguration {
private static final String EXAMPLE_ID = "AKASOMEACCESSKEY";
private static final String EXAMPLE_KEY =
"RGV0cm9pdCBSZ/WQgY2xl/YW5lZCB1cAEXAMPLE";
private Configuration conf; private Configuration conf;
private S3AFileSystem fs; private S3AFileSystem fs;
@ -44,6 +58,9 @@ public class TestS3AConfiguration {
@Rule @Rule
public Timeout testTimeout = new Timeout(30 * 60 * 1000); public Timeout testTimeout = new Timeout(30 * 60 * 1000);
@Rule
public final TemporaryFolder tempDir = new TemporaryFolder();
/** /**
* Test if custom endpoint is picked up. * Test if custom endpoint is picked up.
* <p/> * <p/>
@ -59,7 +76,7 @@ public class TestS3AConfiguration {
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void TestEndpoint() throws Exception { public void testEndpoint() throws Exception {
conf = new Configuration(); conf = new Configuration();
String endpoint = conf.getTrimmed(TEST_ENDPOINT, ""); String endpoint = conf.getTrimmed(TEST_ENDPOINT, "");
if (endpoint.isEmpty()) { if (endpoint.isEmpty()) {
@ -85,7 +102,7 @@ public void TestEndpoint() throws Exception {
} }
@Test @Test
public void TestProxyConnection() throws Exception { public void testProxyConnection() throws Exception {
conf = new Configuration(); conf = new Configuration();
conf.setInt(Constants.MAX_ERROR_RETRIES, 2); conf.setInt(Constants.MAX_ERROR_RETRIES, 2);
conf.set(Constants.PROXY_HOST, "127.0.0.1"); conf.set(Constants.PROXY_HOST, "127.0.0.1");
@ -103,7 +120,7 @@ public void TestProxyConnection() throws Exception {
} }
@Test @Test
public void TestProxyPortWithoutHost() throws Exception { public void testProxyPortWithoutHost() throws Exception {
conf = new Configuration(); conf = new Configuration();
conf.setInt(Constants.MAX_ERROR_RETRIES, 2); conf.setInt(Constants.MAX_ERROR_RETRIES, 2);
conf.setInt(Constants.PROXY_PORT, 1); conf.setInt(Constants.PROXY_PORT, 1);
@ -120,7 +137,7 @@ public void TestProxyPortWithoutHost() throws Exception {
} }
@Test @Test
public void TestAutomaticProxyPortSelection() throws Exception { public void testAutomaticProxyPortSelection() throws Exception {
conf = new Configuration(); conf = new Configuration();
conf.setInt(Constants.MAX_ERROR_RETRIES, 2); conf.setInt(Constants.MAX_ERROR_RETRIES, 2);
conf.set(Constants.PROXY_HOST, "127.0.0.1"); conf.set(Constants.PROXY_HOST, "127.0.0.1");
@ -145,7 +162,7 @@ public void TestAutomaticProxyPortSelection() throws Exception {
} }
@Test @Test
public void TestUsernameInconsistentWithPassword() throws Exception { public void testUsernameInconsistentWithPassword() throws Exception {
conf = new Configuration(); conf = new Configuration();
conf.setInt(Constants.MAX_ERROR_RETRIES, 2); conf.setInt(Constants.MAX_ERROR_RETRIES, 2);
conf.set(Constants.PROXY_HOST, "127.0.0.1"); conf.set(Constants.PROXY_HOST, "127.0.0.1");
@ -177,4 +194,128 @@ public void TestUsernameInconsistentWithPassword() throws Exception {
} }
} }
} }
@Test
public void testCredsFromCredentialProvider() throws Exception {
// set up conf to have a cred provider
final Configuration conf = new Configuration();
final File file = tempDir.newFile("test.jks");
final URI jks = ProviderUtils.nestURIForLocalJavaKeyStoreProvider(
file.toURI());
conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
jks.toString());
provisionAccessKeys(conf);
S3AFileSystem s3afs = new S3AFileSystem();
conf.set(Constants.ACCESS_KEY, EXAMPLE_ID + "LJM");
S3AFileSystem.AWSAccessKeys creds =
s3afs.getAWSAccessKeys(new URI("s3a://foobar"), conf);
assertEquals("AccessKey incorrect.", EXAMPLE_ID, creds.getAccessKey());
assertEquals("SecretKey incorrect.", EXAMPLE_KEY, creds.getAccessSecret());
}
void provisionAccessKeys(final Configuration conf) throws Exception {
// add our creds to the provider
final CredentialProvider provider =
CredentialProviderFactory.getProviders(conf).get(0);
provider.createCredentialEntry(Constants.ACCESS_KEY,
EXAMPLE_ID.toCharArray());
provider.createCredentialEntry(Constants.SECRET_KEY,
EXAMPLE_KEY.toCharArray());
provider.flush();
}
@Test
public void testCredsFromUserInfo() throws Exception {
// set up conf to have a cred provider
final Configuration conf = new Configuration();
final File file = tempDir.newFile("test.jks");
final URI jks = ProviderUtils.nestURIForLocalJavaKeyStoreProvider(
file.toURI());
conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
jks.toString());
provisionAccessKeys(conf);
S3AFileSystem s3afs = new S3AFileSystem();
conf.set(Constants.ACCESS_KEY, EXAMPLE_ID + "LJM");
URI uriWithUserInfo = new URI("s3a://123:456@foobar");
S3AFileSystem.AWSAccessKeys creds =
s3afs.getAWSAccessKeys(uriWithUserInfo, conf);
assertEquals("AccessKey incorrect.", "123", creds.getAccessKey());
assertEquals("SecretKey incorrect.", "456", creds.getAccessSecret());
}
@Test
public void testIDFromUserInfoSecretFromCredentialProvider()
throws Exception {
// set up conf to have a cred provider
final Configuration conf = new Configuration();
final File file = tempDir.newFile("test.jks");
final URI jks = ProviderUtils.nestURIForLocalJavaKeyStoreProvider(
file.toURI());
conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
jks.toString());
provisionAccessKeys(conf);
S3AFileSystem s3afs = new S3AFileSystem();
conf.set(Constants.ACCESS_KEY, EXAMPLE_ID + "LJM");
URI uriWithUserInfo = new URI("s3a://123@foobar");
S3AFileSystem.AWSAccessKeys creds =
s3afs.getAWSAccessKeys(uriWithUserInfo, conf);
assertEquals("AccessKey incorrect.", "123", creds.getAccessKey());
assertEquals("SecretKey incorrect.", EXAMPLE_KEY, creds.getAccessSecret());
}
@Test
public void testSecretFromCredentialProviderIDFromConfig() throws Exception {
// set up conf to have a cred provider
final Configuration conf = new Configuration();
final File file = tempDir.newFile("test.jks");
final URI jks = ProviderUtils.nestURIForLocalJavaKeyStoreProvider(
file.toURI());
conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
jks.toString());
// add our creds to the provider
final CredentialProvider provider =
CredentialProviderFactory.getProviders(conf).get(0);
provider.createCredentialEntry(Constants.SECRET_KEY,
EXAMPLE_KEY.toCharArray());
provider.flush();
S3AFileSystem s3afs = new S3AFileSystem();
conf.set(Constants.ACCESS_KEY, EXAMPLE_ID);
S3AFileSystem.AWSAccessKeys creds =
s3afs.getAWSAccessKeys(new URI("s3a://foobar"), conf);
assertEquals("AccessKey incorrect.", EXAMPLE_ID, creds.getAccessKey());
assertEquals("SecretKey incorrect.", EXAMPLE_KEY, creds.getAccessSecret());
}
@Test
public void testIDFromCredentialProviderSecretFromConfig() throws Exception {
// set up conf to have a cred provider
final Configuration conf = new Configuration();
final File file = tempDir.newFile("test.jks");
final URI jks = ProviderUtils.nestURIForLocalJavaKeyStoreProvider(
file.toURI());
conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
jks.toString());
// add our creds to the provider
final CredentialProvider provider =
CredentialProviderFactory.getProviders(conf).get(0);
provider.createCredentialEntry(Constants.ACCESS_KEY,
EXAMPLE_ID.toCharArray());
provider.flush();
S3AFileSystem s3afs = new S3AFileSystem();
conf.set(Constants.SECRET_KEY, EXAMPLE_KEY);
S3AFileSystem.AWSAccessKeys creds =
s3afs.getAWSAccessKeys(new URI("s3a://foobar"), conf);
assertEquals("AccessKey incorrect.", EXAMPLE_ID, creds.getAccessKey());
assertEquals("SecretKey incorrect.", EXAMPLE_KEY, creds.getAccessSecret());
}
} }