Search code examples
javaamazon-web-servicesaws-java-sdk

AWS Java - How do I load the ~/.aws/config file?


I've been reading through documentation for some time. I can see examples for the JavaScript and Go SDK that show how to load the config file by setting the AWS_SDK_LOAD_CONFIG environment variable to a truthy value. The documentation is here and here respectively.

However, for my requirements, I must use Java. I can't find an equivalent reference in the Java SDK. Which leads me to assume three things.

  1. Java's SDK doesn't use this variable
    • I'm pretty sure this might be the case, as just trying it didn't seem to get it to work.
    • Update: checking out both the Java SDK and Java SDK V2 and searching with ack -i "AWS_SDK_LOAD_CONFIG" shows that neither project use this variable.
  2. Java's SDK uses a different variable
    • I think this is un-likely, as it would not be uniform with the other two SDKs.
  3. Java's SDK expects you to do this programmatically.
    • Seems the most likely, yet I can't find how to do this. I must be using the wrong key-words or be overlooking something to get this behavior.

For clarity the profile I need to load is sbx, which lives in my config, but has no adjacent value in the credentials file. Here is my ~/.aws/config file:

[profile shared]
output = json
region = us-west-2
adfs_config.ssl_verification = True
adfs_config.role_arn = ....
adfs_config.adfs_host = ....
adfs_config.adfs_user = ....

[profile sbx]
role_arn = ... (this is different from the adfs_config.role_arn above)
source_profile = shared
region = us-west-2

and ~/.aws/credentials file: (this file is automatically populated with the aws-adfs command.

[shared]
aws_access_key_id = ....
aws_secret_access_key = ....
aws_session_token = ....
aws_security_token = ....

Solution

  • I figured out the answer. The problem to this issue is that the credentials file is loaded by default, but it doesn't always have all the information available from the config file. We need both to be loaded and flattened.

    AWS already provides ProfileAssumeRoleCredentialsProvider which allows us to assume a Role from a profile. Once we provide it all the information it needs, it can assume the Role without issue (assuming your token is current)

    /**
     * @author Paul Nelson Baker
     * @see <a href="https://github.com/paul-nelson-baker/">GitHub</a>
     * @see <a href="https://www.linkedin.com/in/paul-n-baker/">LinkedIn</a>
     * @since 2018-11
     */
    public class CredentialsChain {
    
      public static final AWSCredentialsProviderChain CREDENTIALS_PROVIDER_CHAIN;
    
      static {
          AllProfiles allProfiles = flattenConfigurationFiles(
                  DEFAULT_CONFIG_LOCATION_PROVIDER.getLocation(), // ~/.aws/config
                  DEFAULT_CREDENTIALS_LOCATION_PROVIDER.getLocation() // ~/.aws/credentials
          );
          String currentProfileName = AwsProfileNameLoader.INSTANCE.loadProfileName();
          BasicProfile currentProfile = allProfiles.getProfile(currentProfileName);
          STSProfileCredentialsService profileCredentialsService = new STSProfileCredentialsService();
          // We stick our merged profile provider first, but we still want the default behavior to apply
          // so create a new chain with the default chain as the tail provider.
          CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain(
                  new ProfileAssumeRoleCredentialsProvider(profileCredentialsService, allProfiles, currentProfile),
                  new DefaultAWSCredentialsProviderChain()
          );
      }
    
      private static AllProfiles flattenConfigurationFiles(File firstFile, File... additionalFiles) {
          // Utilize the AWS SDK to load the actual profile objects
          List<ProfilesConfigFile> allProfileConfigFiles = Stream.concat(Stream.of(firstFile), Arrays.stream(additionalFiles))
                  .map(ProfilesConfigFile::new).collect(Collectors.toList());
          // Process each file one by one, look at their profiles, and place their values into a single map
          // Duplicate profiles will now have the single key/value pairs.
          Map<String, Map<String, String>> buildingMap = new LinkedHashMap<>();
          for (ProfilesConfigFile currentConfigFile : allProfileConfigFiles) {
              for (Entry<String, BasicProfile> currentProfile : currentConfigFile.getAllBasicProfiles().entrySet()) {
                  // Some profiles are prefixed with "profile " so we want to cull it so we're actually merging the correct data
                  String currentProfileName = currentProfile.getKey().replaceAll("^profile\\s+", "");
                  if (!buildingMap.containsKey(currentProfileName)) {
                      buildingMap.put(currentProfileName, new LinkedHashMap<>());
                  }
                  Map<String, String> profileKeyValuePairs = buildingMap.get(currentProfileName);
                  for (Entry<String, String> overridingEntry : currentProfile.getValue().getProperties().entrySet()) {
                      profileKeyValuePairs.put(overridingEntry.getKey(), overridingEntry.getValue());
                  }
              }
          }
          // Take the results, and convert them to AWS SDK Types
          Map<String, BasicProfile> finalResult = new LinkedHashMap<>();
          for (Entry<String, Map<String, String>> currentFinalProfile : buildingMap.entrySet()) {
              String currentProfileName = currentFinalProfile.getKey();
              finalResult.put(currentProfileName, new BasicProfile(currentProfileName, currentFinalProfile.getValue()));
          }
          return new AllProfiles(finalResult);
      }
    
      private CredentialsChain() {
      }
    }