diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-1c5bd30.json b/.changes/next-release/bugfix-AWSSDKforJavav2-1c5bd30.json new file mode 100644 index 000000000000..9cde964ab9c4 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-1c5bd30.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fix credential reloading in defaults when shared credential/config files are modified." +} diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-b3c8f2c.json b/.changes/next-release/bugfix-AWSSDKforJavav2-b3c8f2c.json new file mode 100644 index 000000000000..1c5b1010c20e --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-b3c8f2c.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Gracefully handle missing file in ProfileFileSupplier.reloadWhenModified." +} diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index 70210097c66f..364cd4401529 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -105,7 +105,7 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) { this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; this.asyncThreadName = builder.asyncThreadName; this.profileFile = Optional.ofNullable(builder.profileFile) - .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile())); + .orElseGet(ProfileFileSupplier::defaultSupplier); this.profileName = Optional.ofNullable(builder.profileName) .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow); this.sourceChain = builder.sourceChain; diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java index 95d6bb88afb4..f95dafdb36f3 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java @@ -73,7 +73,7 @@ private ProfileCredentialsProvider(BuilderImpl builder) { .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow); selectedProfileSupplier = Optional.ofNullable(builder.profileFile) - .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(builder.defaultProfileFileLoader.get())); + .orElse(defaultProfileFileLoader); } catch (RuntimeException e) { // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception @@ -216,7 +216,7 @@ public interface Builder extends CopyableBuilder profileFile; private String profileName; - private Supplier defaultProfileFileLoader = ProfileFile::defaultProfileFile; + private Supplier defaultProfileFileLoader = ProfileFileSupplier.defaultSupplier(); BuilderImpl() { } diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java index 0ffad04e77b3..474a546a24df 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -33,6 +34,7 @@ import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.testutils.Waiter; import software.amazon.awssdk.utils.StringInputStream; /** @@ -243,6 +245,32 @@ void resolveCredentials_presentSupplierProfileFile_returnsCredentials() { }); } + @Test + void resolveCredentials_defaultProfileFileSupplier_refreshesCredentials() { + AtomicBoolean firstCall = new AtomicBoolean(true); + ProfileFile file1 = profileFile("[default]\naws_access_key_id = akid1\n" + + "aws_secret_access_key = sak1\n"); + ProfileFile file2 = profileFile("[default]\naws_access_key_id = akid2\n" + + "aws_secret_access_key = sak2\n"); + Supplier refreshingSupplier = () -> firstCall.getAndSet(false) ? file1 : file2; + + ProfileCredentialsProvider provider = new ProfileCredentialsProvider + .BuilderImpl() + .defaultProfileFileLoader(refreshingSupplier) + .profileName("default") + .build(); + + assertThat(provider.resolveCredentials()).satisfies(credentials -> { + assertThat(credentials.accessKeyId()).isEqualTo("akid1"); + assertThat(credentials.secretAccessKey()).isEqualTo("sak1"); + }); + + assertThat(provider.resolveCredentials()).satisfies(credentials -> { + assertThat(credentials.accessKeyId()).isEqualTo("akid2"); + assertThat(credentials.secretAccessKey()).isEqualTo("sak2"); + }); + } + @Test void create_noProfileName_returnsProfileCredentialsProviderToResolveWithDefaults() { ProfileCredentialsProvider provider = ProfileCredentialsProvider.create(); diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java index 7046c300d339..4ea4cd34989c 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.profiles; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.LinkedHashMap; @@ -38,7 +39,7 @@ public interface ProfileFileSupplier extends Supplier { /** * Creates a {@link ProfileFileSupplier} capable of producing multiple profile objects by aggregating the default - * credentials and configuration files as determined by {@link ProfileFileLocation#credentialsFileLocation()} abd + * credentials and configuration files as determined by {@link ProfileFileLocation#credentialsFileLocation()} and * {@link ProfileFileLocation#configurationFileLocation()}. This supplier will return a new ProfileFile instance only once * either disk file has been modified. Multiple calls to the supplier while both disk files are unchanged will return the * same object. @@ -81,13 +82,17 @@ static ProfileFileSupplier defaultSupplier() { */ static ProfileFileSupplier reloadWhenModified(Path path, ProfileFile.Type type) { return new ProfileFileSupplier() { - - final ProfileFile.Builder builder = ProfileFile.builder() - .content(path) - .type(type); + Supplier profileFileSupplier = () -> { + if (Files.isRegularFile(path) && Files.isReadable(path)) { + return ProfileFile.builder() + .content(path) + .type(type).build(); + } + return ProfileFile.empty(); + }; final ProfileFileRefresher refresher = ProfileFileRefresher.builder() - .profileFile(builder::build) + .profileFile(profileFileSupplier) .profileFilePath(path) .build(); diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java index 6e3aea7cf5bf..7ca0f0012dc2 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.profiles; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Objects; @@ -33,10 +34,14 @@ final class ProfileFileSupplierBuilder { private Consumer onProfileFileLoad; public ProfileFileSupplierBuilder reloadWhenModified(Path path, ProfileFile.Type type) { - ProfileFile.Builder builder = ProfileFile.builder() - .content(path) - .type(type); - this.profileFile = builder::build; + this.profileFile = () -> { + if (Files.isRegularFile(path) && Files.isReadable(path)) { + return ProfileFile.builder() + .content(path) + .type(type).build(); + } + return ProfileFile.empty(); + }; this.profileFilePath = path; this.reloadingSupplier = true; return this; diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java index 60a91a25527f..56a3e4614a5a 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java @@ -122,7 +122,7 @@ private boolean isNewProfileFile(ProfileFile profileFile) { } private boolean canReloadProfileFile() { - if (Objects.isNull(profileFilePath)) { + if (Objects.isNull(profileFilePath) || !Files.exists(profileFilePath)) { return false; } diff --git a/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java b/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java index e5ca165840ae..5a725914aba1 100644 --- a/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java +++ b/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java @@ -592,6 +592,28 @@ public void defaultSupplier_noCredentialsFiles_returnsEmptyProvider() { }); } + @Test + public void reloadWhenModified_noCredentialsFiles_returnsEmptyProvider_andRefreshes() throws IOException { + Path credentialsFilePath = getTestCredentialsFilePath(); + Files.deleteIfExists(credentialsFilePath); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileSupplier supplier = builderWithClock(clock) + .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS) + .build(); + + assertThat(supplier.get().profiles()).isEmpty(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey", "modifiedAccountId"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(Duration.ofSeconds(10)); + + // supplied ProfileFile should refreshed and now have data under the `default` profile + Optional fileOptional = supplier.get().profile("default"); + assertThat(fileOptional).isPresent(); + } + private Path writeTestFile(String contents, Path path) { try { Files.createDirectories(testDirectory); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index ad4e7bcbdec4..076f79b44c89 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -105,13 +105,13 @@ import software.amazon.awssdk.identity.spi.IdentityProviders; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.profiles.ProfileProperty; import software.amazon.awssdk.retries.api.RetryStrategy; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.AttributeMap.LazyValueSource; import software.amazon.awssdk.utils.Either; -import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.OptionalUtils; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.ThreadFactoryBuilder; @@ -280,7 +280,7 @@ protected SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration confi * Apply global default configuration */ private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration configuration) { - Supplier defaultProfileFileSupplier = new Lazy<>(ProfileFile::defaultProfileFile)::getValue; + Supplier defaultProfileFileSupplier = ProfileFileSupplier.defaultSupplier(); configuration = configuration.merge(c -> c.option(EXECUTION_INTERCEPTORS, new ArrayList<>()) .option(METRIC_PUBLISHERS, new ArrayList<>()) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index 25e83b36383c..973fc5a92ce5 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -44,8 +44,12 @@ import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.io.File; +import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -59,7 +63,9 @@ import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; @@ -81,6 +87,8 @@ import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.testutils.Waiter; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.StringInputStream; @@ -100,6 +108,9 @@ public class DefaultClientBuilderTest { private static final URI ENDPOINT = URI.create("https://example.com"); private static final NoOpSigner TEST_SIGNER = new NoOpSigner(); + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + @Mock private SdkHttpClient.Builder defaultHttpClientFactory; @@ -437,6 +448,13 @@ private SdkDefaultClientBuilder testAsy return new TestAsyncClientBuilder().overrideConfiguration(overrideConfig); } + private void writeTestCredentialsFile(File file, String accessKeyId, String secretAccessKey) + throws IOException { + String contents = String.format("[default]\naws_access_key_id = %s\naws_secret_access_key = %s\n", + accessKeyId, secretAccessKey); + Files.write(file.toPath(), contents.getBytes(StandardCharsets.UTF_8)); + } + private static class TestClient { private final SdkClientConfiguration clientConfiguration; diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index be6532fac420..cb3064798a99 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -121,7 +121,7 @@ private S3Utilities(Builder builder) { this.region = Validate.paramNotNull(builder.region, "Region"); this.endpoint = builder.endpoint; this.profileFile = Optional.ofNullable(builder.profileFile) - .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile())); + .orElseGet(ProfileFileSupplier::defaultSupplier); this.profileName = builder.profileName; if (builder.s3Configuration == null) {