Skip to content

Commit e40f696

Browse files
authored
HADOOP-18980. S3A credential provider remapping: make extensible (#6406)
Contributed by Viraj Jasani
1 parent 2d14dbc commit e40f696

File tree

10 files changed

+376
-3
lines changed

10 files changed

+376
-3
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/conf/Configuration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2339,8 +2339,8 @@ public Collection<String> getTrimmedStringCollection(String name) {
23392339
}
23402340
return StringUtils.getTrimmedStringCollection(valueString);
23412341
}
2342-
2343-
/**
2342+
2343+
/**
23442344
* Get the comma delimited values of the <code>name</code> property as
23452345
* an array of <code>String</code>s, trimmed of the leading and trailing whitespace.
23462346
* If no such property is specified then an empty array is returned.

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/StringUtils.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
2727
import java.util.Collection;
28+
import java.util.HashMap;
2829
import java.util.Iterator;
2930
import java.util.LinkedHashSet;
3031
import java.util.List;
@@ -479,7 +480,28 @@ public static Collection<String> getTrimmedStringCollection(String str){
479480
set.remove("");
480481
return set;
481482
}
482-
483+
484+
/**
485+
* Splits an "=" separated value <code>String</code>, trimming leading and
486+
* trailing whitespace on each value after splitting by comma and new line separator.
487+
*
488+
* @param str a comma separated <code>String</code> with values, may be null
489+
* @return a <code>Map</code> of <code>String</code> keys and values, empty
490+
* Collection if null String input.
491+
*/
492+
public static Map<String, String> getTrimmedStringCollectionSplitByEquals(
493+
String str) {
494+
String[] trimmedList = getTrimmedStrings(str);
495+
Map<String, String> pairs = new HashMap<>();
496+
for (String s : trimmedList) {
497+
String[] splitByKeyVal = getTrimmedStringsSplitByEquals(s);
498+
if (splitByKeyVal.length == 2) {
499+
pairs.put(splitByKeyVal[0], splitByKeyVal[1]);
500+
}
501+
}
502+
return pairs;
503+
}
504+
483505
/**
484506
* Splits a comma or newline separated value <code>String</code>, trimming
485507
* leading and trailing whitespace on each value.
@@ -497,6 +519,22 @@ public static String[] getTrimmedStrings(String str){
497519
return str.trim().split("\\s*[,\n]\\s*");
498520
}
499521

522+
/**
523+
* Splits "=" separated value <code>String</code>, trimming
524+
* leading and trailing whitespace on each value.
525+
*
526+
* @param str an "=" separated <code>String</code> with values,
527+
* may be null
528+
* @return an array of <code>String</code> values, empty array if null String
529+
* input
530+
*/
531+
public static String[] getTrimmedStringsSplitByEquals(String str){
532+
if (null == str || str.trim().isEmpty()) {
533+
return emptyStringArray;
534+
}
535+
return str.trim().split("\\s*=\\s*");
536+
}
537+
500538
final public static String[] emptyStringArray = {};
501539
final public static char COMMA = ',';
502540
final public static String COMMA_STR = ",";

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestStringUtils.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.apache.commons.lang3.time.FastDateFormat;
4545
import org.apache.hadoop.test.UnitTestcaseTimeLimit;
4646
import org.apache.hadoop.util.StringUtils.TraditionalBinaryPrefix;
47+
48+
import org.assertj.core.api.Assertions;
4749
import org.junit.Test;
4850

4951
public class TestStringUtils extends UnitTestcaseTimeLimit {
@@ -512,6 +514,60 @@ public void testCreateStartupShutdownMessage() {
512514
assertTrue(msg.startsWith("STARTUP_MSG:"));
513515
}
514516

517+
@Test
518+
public void testStringCollectionSplitByEquals() {
519+
Map<String, String> splitMap =
520+
StringUtils.getTrimmedStringCollectionSplitByEquals("");
521+
Assertions
522+
.assertThat(splitMap)
523+
.describedAs("Map of key value pairs split by equals(=) and comma(,)")
524+
.hasSize(0);
525+
526+
splitMap = StringUtils.getTrimmedStringCollectionSplitByEquals(null);
527+
Assertions
528+
.assertThat(splitMap)
529+
.describedAs("Map of key value pairs split by equals(=) and comma(,)")
530+
.hasSize(0);
531+
532+
splitMap = StringUtils.getTrimmedStringCollectionSplitByEquals(
533+
"element.first.key1 = element.first.val1");
534+
Assertions
535+
.assertThat(splitMap)
536+
.describedAs("Map of key value pairs split by equals(=) and comma(,)")
537+
.hasSize(1)
538+
.containsEntry("element.first.key1", "element.first.val1");
539+
540+
splitMap = StringUtils.getTrimmedStringCollectionSplitByEquals(
541+
"element.xyz.key1 =element.abc.val1 , element.xyz.key2= element.abc.val2");
542+
543+
Assertions
544+
.assertThat(splitMap)
545+
.describedAs("Map of key value pairs split by equals(=) and comma(,)")
546+
.hasSize(2)
547+
.containsEntry("element.xyz.key1", "element.abc.val1")
548+
.containsEntry("element.xyz.key2", "element.abc.val2");
549+
550+
splitMap = StringUtils.getTrimmedStringCollectionSplitByEquals(
551+
"\nelement.xyz.key1 =element.abc.val1 \n"
552+
+ ", element.xyz.key2=element.abc.val2,element.xyz.key3=element.abc.val3"
553+
+ " , element.xyz.key4 =element.abc.val4,element.xyz.key5= "
554+
+ "element.abc.val5 ,\n \n \n "
555+
+ " element.xyz.key6 = element.abc.val6 \n , \n"
556+
+ "element.xyz.key7=element.abc.val7,\n");
557+
558+
Assertions
559+
.assertThat(splitMap)
560+
.describedAs("Map of key value pairs split by equals(=) and comma(,)")
561+
.hasSize(7)
562+
.containsEntry("element.xyz.key1", "element.abc.val1")
563+
.containsEntry("element.xyz.key2", "element.abc.val2")
564+
.containsEntry("element.xyz.key3", "element.abc.val3")
565+
.containsEntry("element.xyz.key4", "element.abc.val4")
566+
.containsEntry("element.xyz.key5", "element.abc.val5")
567+
.containsEntry("element.xyz.key6", "element.abc.val6")
568+
.containsEntry("element.xyz.key7", "element.abc.val7");
569+
}
570+
515571
// Benchmark for StringUtils split
516572
public static void main(String []args) {
517573
final String TO_SPLIT = "foo,bar,baz,blah,blah";

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ private Constants() {
6868
public static final String AWS_CREDENTIALS_PROVIDER =
6969
"fs.s3a.aws.credentials.provider";
7070

71+
/**
72+
* AWS credentials providers mapping with key/value pairs.
73+
* Value = {@value}
74+
*/
75+
public static final String AWS_CREDENTIALS_PROVIDER_MAPPING =
76+
"fs.s3a.aws.credentials.provider.mapping";
77+
7178
/**
7279
* Extra set of security credentials which will be prepended to that
7380
* set in {@code "hadoop.security.credential.provider.path"}.

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import java.util.ArrayList;
6767
import java.util.Collection;
6868
import java.util.Date;
69+
import java.util.HashMap;
6970
import java.util.List;
7071
import java.util.Map;
7172
import java.util.Optional;
@@ -1670,6 +1671,28 @@ public static String formatRange(long rangeStart, long rangeEnd) {
16701671
return String.format("bytes=%d-%d", rangeStart, rangeEnd);
16711672
}
16721673

1674+
/**
1675+
* Get the equal op (=) delimited key-value pairs of the <code>name</code> property as
1676+
* a collection of pair of <code>String</code>s, trimmed of the leading and trailing whitespace
1677+
* after delimiting the <code>name</code> by comma and new line separator.
1678+
* If no such property is specified then empty <code>Map</code> is returned.
1679+
*
1680+
* @param configuration the configuration object.
1681+
* @param name property name.
1682+
* @return property value as a <code>Map</code> of <code>String</code>s, or empty
1683+
* <code>Map</code>.
1684+
*/
1685+
public static Map<String, String> getTrimmedStringCollectionSplitByEquals(
1686+
final Configuration configuration,
1687+
final String name) {
1688+
String valueString = configuration.get(name);
1689+
if (null == valueString) {
1690+
return new HashMap<>();
1691+
}
1692+
return org.apache.hadoop.util.StringUtils
1693+
.getTrimmedStringCollectionSplitByEquals(valueString);
1694+
}
1695+
16731696
/**
16741697
* If classloader isolation is {@code true}
16751698
* (through {@link Constants#AWS_S3_CLASSLOADER_ISOLATION}) or not

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/CredentialProviderListFactory.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.hadoop.fs.store.LogExactlyOnce;
5252

5353
import static org.apache.hadoop.fs.s3a.Constants.AWS_CREDENTIALS_PROVIDER;
54+
import static org.apache.hadoop.fs.s3a.Constants.AWS_CREDENTIALS_PROVIDER_MAPPING;
5455
import static org.apache.hadoop.fs.s3a.adapter.AwsV1BindingSupport.isAwsV1SdkAvailable;
5556

5657
/**
@@ -216,6 +217,9 @@ public static AWSCredentialProviderList buildAWSProviderList(
216217
key,
217218
defaultValues.toArray(new Class[defaultValues.size()]));
218219

220+
Map<String, String> awsCredsMappedClasses =
221+
S3AUtils.getTrimmedStringCollectionSplitByEquals(conf,
222+
AWS_CREDENTIALS_PROVIDER_MAPPING);
219223
Map<String, String> v1v2CredentialProviderMap = V1_V2_CREDENTIAL_PROVIDER_MAP;
220224
final Set<String> forbiddenClassnames =
221225
forbidden.stream().map(c -> c.getName()).collect(Collectors.toSet());
@@ -232,6 +236,10 @@ public static AWSCredentialProviderList buildAWSProviderList(
232236
LOG_REMAPPED_ENTRY.warn("Credentials option {} contains AWS v1 SDK entry {}; mapping to {}",
233237
key, className, mapped);
234238
className = mapped;
239+
} else if (awsCredsMappedClasses != null && awsCredsMappedClasses.containsKey(className)) {
240+
final String mapped = awsCredsMappedClasses.get(className);
241+
LOG_REMAPPED_ENTRY.debug("Credential entry {} is mapped to {}", className, mapped);
242+
className = mapped;
235243
}
236244
// now scan the forbidden list. doing this after any mappings ensures the v1 names
237245
// are also blocked

hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/aws_sdk_upgrade.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,55 @@ The change in interface will mean that custom credential providers will need to
6666
implement `software.amazon.awssdk.auth.credentials.AwsCredentialsProvider` instead of
6767
`com.amazonaws.auth.AWSCredentialsProvider`.
6868

69+
[HADOOP-18980](https://issues.apache.org/jira/browse/HADOOP-18980) introduces extended version of
70+
the credential provider remapping. `fs.s3a.aws.credentials.provider.mapping` can be used to
71+
list comma-separated key-value pairs of mapped credential providers that are separated by
72+
equal operator (=).
73+
The key can be used by `fs.s3a.aws.credentials.provider` or
74+
`fs.s3a.assumed.role.credentials.provider` configs, and the key will be translated into
75+
the specified value of credential provider class based on the key-value pair
76+
provided by the config `fs.s3a.aws.credentials.provider.mapping`.
77+
78+
For example, if `fs.s3a.aws.credentials.provider.mapping` is set with value:
79+
80+
```xml
81+
<property>
82+
<name>fs.s3a.aws.credentials.provider.mapping</name>
83+
<vale>
84+
com.amazonaws.auth.AnonymousAWSCredentials=org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider,
85+
com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper=org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider,
86+
com.amazonaws.auth.InstanceProfileCredentialsProvider=org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider
87+
</vale>
88+
</property>
89+
```
90+
91+
and if `fs.s3a.aws.credentials.provider` is set with:
92+
93+
```xml
94+
<property>
95+
<name>fs.s3a.aws.credentials.provider</name>
96+
<vale>com.amazonaws.auth.AnonymousAWSCredentials</vale>
97+
</property>
98+
```
99+
100+
`com.amazonaws.auth.AnonymousAWSCredentials` will be internally remapped to
101+
`org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider` by S3A while preparing
102+
the AWS credential provider list.
103+
104+
Similarly, if `fs.s3a.assumed.role.credentials.provider` is set with:
105+
106+
```xml
107+
<property>
108+
<name>fs.s3a.assumed.role.credentials.provider</name>
109+
<vale>com.amazonaws.auth.InstanceProfileCredentialsProvider</vale>
110+
</property>
111+
```
112+
113+
`com.amazonaws.auth.InstanceProfileCredentialsProvider` will be internally
114+
remapped to `org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider` by
115+
S3A while preparing the assumed role AWS credential provider list.
116+
117+
69118
### Original V1 `AWSCredentialsProvider` interface
70119

71120
Note how the interface begins with the capitalized "AWS" acronym.

hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,28 @@ For more information see [Upcoming upgrade to AWS Java SDK V2](./aws_sdk_upgrade
282282
credentials.
283283
</description>
284284
</property>
285+
286+
<property>
287+
<name>fs.s3a.aws.credentials.provider.mapping</name>
288+
<description>
289+
Comma-separated key-value pairs of mapped credential providers that are
290+
separated by equal operator (=). The key can be used by
291+
fs.s3a.aws.credentials.provider config, and it will be translated into
292+
the specified value of credential provider class based on the key-value
293+
pair provided by this config.
294+
295+
Example:
296+
com.amazonaws.auth.AnonymousAWSCredentials=org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider,
297+
com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper=org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider,
298+
com.amazonaws.auth.InstanceProfileCredentialsProvider=org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider
299+
300+
With the above key-value pairs, if fs.s3a.aws.credentials.provider specifies
301+
com.amazonaws.auth.AnonymousAWSCredentials, it will be remapped to
302+
org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider by S3A while
303+
preparing AWS credential provider list for any S3 access.
304+
We can use the same credentials provider list for both v1 and v2 SDK clients.
305+
</description>
306+
</property>
285307
```
286308

287309
### <a name="auth_env_vars"></a> Authenticating via the AWS Environment Variables

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AAWSCredentialsProvider.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ public void testBadCredentialsConstructor() throws Exception {
108108
}
109109
}
110110

111+
/**
112+
* Test aws credentials provider remapping with key that maps to
113+
* BadCredentialsProviderConstructor.
114+
*/
115+
@Test
116+
public void testBadCredentialsConstructorWithRemap() throws Exception {
117+
Configuration conf = createConf("aws.test.map1");
118+
conf.set(AWS_CREDENTIALS_PROVIDER_MAPPING,
119+
"aws.test.map1=" + BadCredentialsProviderConstructor.class.getName());
120+
final InstantiationIOException ex =
121+
intercept(InstantiationIOException.class, CONSTRUCTOR_EXCEPTION, () ->
122+
createFailingFS(conf));
123+
if (InstantiationIOException.Kind.UnsupportedConstructor != ex.getKind()) {
124+
throw ex;
125+
}
126+
}
127+
111128
/**
112129
* Create a configuration bonded to the given provider classname.
113130
* @param provider provider to bond to
@@ -169,6 +186,20 @@ public void testBadCredentials() throws Exception {
169186
createFailingFS(conf));
170187
}
171188

189+
/**
190+
* Test aws credentials provider remapping with key that maps to
191+
* BadCredentialsProvider.
192+
*/
193+
@Test
194+
public void testBadCredentialsWithRemap() throws Exception {
195+
Configuration conf = createConf("aws.test.map.key");
196+
conf.set(AWS_CREDENTIALS_PROVIDER_MAPPING,
197+
"aws.test.map.key=" + BadCredentialsProvider.class.getName());
198+
intercept(AccessDeniedException.class,
199+
"",
200+
() -> createFailingFS(conf));
201+
}
202+
172203
/**
173204
* Test using the anonymous credential provider with the public csv
174205
* test file; if the test file path is unset then it will be skipped.

0 commit comments

Comments
 (0)