Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-688d30e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS SDK for Java v2",
"contributor": "",
"type": "feature",
"description": "Created ReloadingProfileCredentialsProvider to reload credentials due to disk changes"
}
6 changes: 6 additions & 0 deletions core/auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<version>${jimfs.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
public interface AwsCredentialsProvider {
/**
* Returns {@link AwsCredentials} that can be used to authorize an AWS request. Each implementation of AWSCredentialsProvider
* can chose its own strategy for loading credentials. For example, an implementation might load credentials from an existing
* can choose its own strategy for loading credentials. For example, an implementation might load credentials from an existing
* key management system, or load new credentials when credentials are rotated.
*
* <p>If an error occurs during the loading of credentials or credentials could not be found, a runtime exception will be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.auth.credentials;

import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
Expand All @@ -23,6 +24,7 @@
import software.amazon.awssdk.auth.credentials.internal.ProfileCredentialsUtils;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.SdkAutoCloseable;
Expand All @@ -46,55 +48,42 @@ public final class ProfileCredentialsProvider
implements AwsCredentialsProvider,
SdkAutoCloseable,
ToCopyableBuilder<ProfileCredentialsProvider.Builder, ProfileCredentialsProvider> {
private final AwsCredentialsProvider credentialsProvider;
private final RuntimeException loadException;

private final ProfileFile profileFile;
private AwsCredentialsProvider credentialsProvider;
private final RuntimeException loadException;
private final ProfileFileSupplier profileFileSupplier;
private volatile ProfileFile currentProfileFile;
private final String profileName;

private final Supplier<ProfileFile> defaultProfileFileLoader;

/**
* @see #builder()
*/
private ProfileCredentialsProvider(BuilderImpl builder) {
AwsCredentialsProvider credentialsProvider = null;
RuntimeException loadException = null;
ProfileFile profileFile = null;
String profileName = null;
this.defaultProfileFileLoader = builder.defaultProfileFileLoader;

RuntimeException thrownException = null;
String selectedProfileName = null;
ProfileFileSupplier selectedProfileSupplier = null;

try {
profileName = builder.profileName != null ? builder.profileName
: ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();

// Load the profiles file
profileFile = Optional.ofNullable(builder.profileFile)
.orElseGet(builder.defaultProfileFileLoader);

// Load the profile and credentials provider
String finalProfileName = profileName;
ProfileFile finalProfileFile = profileFile;
credentialsProvider =
profileFile.profile(profileName)
.flatMap(p -> new ProfileCredentialsUtils(finalProfileFile, p, finalProfileFile::profile)
.credentialsProvider())
.orElseThrow(() -> {
String errorMessage = String.format("Profile file contained no credentials for " +
"profile '%s': %s", finalProfileName, finalProfileFile);
return SdkClientException.builder().message(errorMessage).build();
});
selectedProfileName = Optional.ofNullable(builder.profileName)
.orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow);

selectedProfileSupplier = Optional.ofNullable(builder.profileFileSupplier)
.orElseGet(() -> ProfileFileSupplier
.fixedProfileFile(builder.defaultProfileFileLoader.get()));

} catch (RuntimeException e) {
// If we couldn't load the credentials provider for some reason, save an exception describing why. This exception
// will only be raised on calls to getCredentials. We don't want to raise an exception here because it may be
// will only be raised on calls to resolveCredentials. We don't want to raise an exception here because it may be
// expected (eg. in the default credential chain).
loadException = e;
thrownException = e;
}

this.loadException = loadException;
this.credentialsProvider = credentialsProvider;
this.profileFile = profileFile;
this.profileName = profileName;
this.defaultProfileFileLoader = builder.defaultProfileFileLoader;
this.loadException = thrownException;
this.profileName = selectedProfileName;
this.profileFileSupplier = selectedProfileSupplier;
}

/**
Expand Down Expand Up @@ -127,19 +116,39 @@ public AwsCredentials resolveCredentials() {
if (loadException != null) {
throw loadException;
}

ProfileFile cachedOrRefreshedProfileFile = refreshProfileFile();
if (isNewProfileFile(cachedOrRefreshedProfileFile)) {
currentProfileFile = cachedOrRefreshedProfileFile;
handleProfileFileReload(cachedOrRefreshedProfileFile);
}

return credentialsProvider.resolveCredentials();
}

private void handleProfileFileReload(ProfileFile profileFile) {
credentialsProvider = createCredentialsProvider(profileFile, profileName);
}

private ProfileFile refreshProfileFile() {
return profileFileSupplier.get();
}

private boolean isNewProfileFile(ProfileFile profileFile) {
return !Objects.equals(currentProfileFile, profileFile);
}

@Override
public String toString() {
return ToString.builder("ProfileCredentialsProvider")
.add("profileName", profileName)
.add("profileFile", profileFile)
.add("profileFile", currentProfileFile)
.build();
}

@Override
public void close() {
profileFileSupplier.close();
// The delegate credentials provider may be closeable (eg. if it's an STS credentials provider). In this case, we should
// clean it up when this credentials provider is closed.
IoUtils.closeIfCloseable(credentialsProvider, null);
Expand All @@ -150,6 +159,17 @@ public Builder toBuilder() {
return new BuilderImpl(this);
}

private AwsCredentialsProvider createCredentialsProvider(ProfileFile profileFile, String profileName) {
// Load the profile and credentials provider
return profileFile.profile(profileName)
.flatMap(p -> new ProfileCredentialsUtils(profileFile, p, profileFile::profile).credentialsProvider())
.orElseThrow(() -> {
String errorMessage = String.format("Profile file contained no credentials for " +
"profile '%s': %s", profileName, profileFile);
return SdkClientException.builder().message(errorMessage).build();
});
}

/**
* A builder for creating a custom {@link ProfileCredentialsProvider}.
*/
Expand All @@ -158,6 +178,7 @@ public interface Builder extends CopyableBuilder<Builder, ProfileCredentialsProv
/**
* Define the profile file that should be used by this credentials provider. By default, the
* {@link ProfileFile#defaultProfileFile()} is used.
* @see #profileFile(ProfileFileSupplier)
*/
Builder profileFile(ProfileFile profileFile);

Expand All @@ -167,6 +188,14 @@ public interface Builder extends CopyableBuilder<Builder, ProfileCredentialsProv
*/
Builder profileFile(Consumer<ProfileFile.Builder> profileFile);

/**
* Define the mechanism for loading profile files.
*
* @param profileFileSupplier Supplier interface for generating a ProfileFile instance.
* @see #profileFile(ProfileFile)
*/
Builder profileFile(ProfileFileSupplier profileFileSupplier);

/**
* Define the name of the profile that should be used by this credentials provider. By default, the value in
* {@link ProfileFileSystemSetting#AWS_PROFILE} is used.
Expand All @@ -176,27 +205,27 @@ public interface Builder extends CopyableBuilder<Builder, ProfileCredentialsProv
/**
* Create a {@link ProfileCredentialsProvider} using the configuration applied to this builder.
*/
@Override
ProfileCredentialsProvider build();
}

static final class BuilderImpl implements Builder {
private ProfileFile profileFile;
private ProfileFileSupplier profileFileSupplier;
private String profileName;
private Supplier<ProfileFile> defaultProfileFileLoader = ProfileFile::defaultProfileFile;

BuilderImpl() {
}

BuilderImpl(ProfileCredentialsProvider provider) {
this.profileFile = provider.profileFile;
this.profileName = provider.profileName;
this.defaultProfileFileLoader = provider.defaultProfileFileLoader;
this.profileFileSupplier = provider.profileFileSupplier;
}

@Override
public Builder profileFile(ProfileFile profileFile) {
this.profileFile = profileFile;
return this;
return profileFile(ProfileFileSupplier.wrapIntoNullableSupplier(profileFile));
}

public void setProfileFile(ProfileFile profileFile) {
Expand All @@ -208,6 +237,16 @@ public Builder profileFile(Consumer<ProfileFile.Builder> profileFile) {
return profileFile(ProfileFile.builder().applyMutation(profileFile).build());
}

@Override
public Builder profileFile(ProfileFileSupplier profileFileSupplier) {
this.profileFileSupplier = profileFileSupplier;
return this;
}

public void setProfileFile(ProfileFileSupplier supplier) {
profileFile(supplier);
}

@Override
public Builder profileName(String profileName) {
this.profileName = profileName;
Expand All @@ -225,13 +264,15 @@ public ProfileCredentialsProvider build() {

/**
* Override the default configuration file to be used when the customer does not explicitly set
* profileName(profileName);
* {@link #profileFile(ProfileFile)}. Use of this method is only useful for testing the default behavior.
* profileFile(ProfileFile) or profileFileSupplier(supplier);
* {@link #profileFile(ProfileFile)}. Use of this method is
* only useful for testing the default behavior.
*/
@SdkTestInternalApi
Builder defaultProfileFileLoader(Supplier<ProfileFile> defaultProfileFileLoader) {
this.defaultProfileFileLoader = defaultProfileFileLoader;
return this;
}
}

}
Loading