TestV1CredentialsProvider.java

/**
 * 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.hadoop.fs.s3a.adapter;

import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.s3a.AWSCredentialProviderList;
import org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider;
import org.apache.hadoop.fs.s3a.auth.IAMInstanceCredentialsProvider;
import org.apache.hadoop.fs.s3a.impl.InstantiationIOException;
import org.apache.hadoop.fs.s3a.test.PublicDatasetTestUtils;

import static org.apache.hadoop.fs.s3a.Constants.AWS_CREDENTIALS_PROVIDER;
import static org.apache.hadoop.fs.s3a.auth.CredentialProviderListFactory.ANONYMOUS_CREDENTIALS_V1;
import static org.apache.hadoop.fs.s3a.auth.CredentialProviderListFactory.EC2_CONTAINER_CREDENTIALS_V1;
import static org.apache.hadoop.fs.s3a.auth.CredentialProviderListFactory.ENVIRONMENT_CREDENTIALS_V1;
import static org.apache.hadoop.fs.s3a.auth.CredentialProviderListFactory.createAWSCredentialProviderList;
import static org.apache.hadoop.test.LambdaTestUtils.intercept;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * Unit tests for v1 to v2 credential provider logic.
 */
public class TestV1CredentialsProvider {

  /**
   * URI of the test file.
   */
  private static final URI TESTFILE_URI = new Path(
      PublicDatasetTestUtils.DEFAULT_EXTERNAL_FILE).toUri();

  private static final Logger LOG = LoggerFactory.getLogger(TestV1CredentialsProvider.class);


  @Test
  public void testV1V2Mapping() throws Exception {
    URI uri1 = new URI("s3a://bucket1");

    List<Class<?>> expectedClasses =
        Arrays.asList(
            IAMInstanceCredentialsProvider.class,
            AnonymousAWSCredentialsProvider.class,
            EnvironmentVariableCredentialsProvider.class);
    Configuration conf =
        createProviderConfiguration(buildClassList(
            EC2_CONTAINER_CREDENTIALS_V1,
            ANONYMOUS_CREDENTIALS_V1,
            ENVIRONMENT_CREDENTIALS_V1));
    AWSCredentialProviderList list1 = createAWSCredentialProviderList(
        uri1, conf);
    assertCredentialProviders(expectedClasses, list1);
  }

  @Test
  public void testV1Wrapping() throws Exception {
    URI uri1 = new URI("s3a://bucket1");

    List<Class<?>> expectedClasses =
        Arrays.asList(
            V1ToV2AwsCredentialProviderAdapter.class,
            V1ToV2AwsCredentialProviderAdapter.class);
    Configuration conf =
        createProviderConfiguration(buildClassList(
            LegacyV1CredentialProvider.class.getName(),
            LegacyV1CredentialProviderWithConf.class.getName()));
    AWSCredentialProviderList list1 = createAWSCredentialProviderList(
        uri1, conf);
    assertCredentialProviders(expectedClasses, list1);
  }

  private String buildClassList(String... classes) {
    return Arrays.stream(classes)
        .collect(Collectors.joining(","));
  }


  /**
   * Expect a provider to raise an exception on failure.
   * @param option aws provider option string.
   * @param expectedErrorText error text to expect
   * @return the exception raised
   * @throws Exception any unexpected exception thrown.
   */
  private IOException expectProviderInstantiationFailure(String option,
      String expectedErrorText) throws Exception {
    return intercept(IOException.class, expectedErrorText,
        () -> createAWSCredentialProviderList(
            TESTFILE_URI,
            createProviderConfiguration(option)));
  }

  /**
   * Create a configuration with a specific provider.
   * @param providerOption option for the aws credential provider option.
   * @return a configuration to use in test cases
   */
  private Configuration createProviderConfiguration(
      final String providerOption) {
    Configuration conf = new Configuration(false);
    conf.set(AWS_CREDENTIALS_PROVIDER, providerOption);
    return conf;
  }

  /**
   * Asserts expected provider classes in list.
   * @param expectedClasses expected provider classes
   * @param list providers to check
   */
  private static void assertCredentialProviders(
      List<Class<?>> expectedClasses,
      AWSCredentialProviderList list) {
    assertNotNull(list);
    List<AwsCredentialsProvider> providers = list.getProviders();
    Assertions.assertThat(providers)
        .describedAs("providers")
        .hasSize(expectedClasses.size());
    for (int i = 0; i < expectedClasses.size(); ++i) {
      Class<?> expectedClass =
          expectedClasses.get(i);
      AwsCredentialsProvider provider = providers.get(i);
      assertNotNull(
          String.format("At position %d, expected class is %s, but found null.",
              i, expectedClass), provider);
      assertTrue(
          String.format("At position %d, expected class is %s, but found %s.",
              i, expectedClass, provider.getClass()),
          expectedClass.isAssignableFrom(provider.getClass()));
    }
  }


  public static class LegacyV1CredentialProvider implements AWSCredentialsProvider {

    public LegacyV1CredentialProvider() {
    }

    @Override
    public AWSCredentials getCredentials() {
      return null;
    }

    @Override
    public void refresh() {

    }
  }

  /**
   * V1 credentials with a configuration constructor.
   */
  public static final class LegacyV1CredentialProviderWithConf
      extends LegacyV1CredentialProvider {

    public LegacyV1CredentialProviderWithConf(Configuration conf) {
    }
  }

  /**
   * V1 Credentials whose factory method raises ClassNotFoundException.
   * Expect this to fail rather than trigger recursive recovery;
   * exception will be wrapped with something intended to be informative.
   */
  @Test
  public void testV1InstantiationFailurePropagation() throws Throwable {
    InstantiationIOException expected = intercept(InstantiationIOException.class,
        "simulated CNFE",
        () -> createAWSCredentialProviderList(
            TESTFILE_URI,
            createProviderConfiguration(V1CredentialProviderDoesNotInstantiate.class.getName())));
    // print for the curious
    LOG.info("{}", expected.toString());
  }


  /**
   * V1 credentials which raises an instantiation exception.
   */
  public static final class V1CredentialProviderDoesNotInstantiate
      extends LegacyV1CredentialProvider {

    private V1CredentialProviderDoesNotInstantiate() {
    }

    public static AWSCredentialsProvider getInstance() throws ClassNotFoundException {
      throw new ClassNotFoundException("simulated CNFE");
    }
  }


}