Skip to content

ProfileTokenProvider Reload ProfileFile #3608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 20, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,53 +40,41 @@ public final class ProfileTokenProvider implements SdkTokenProvider, SdkAutoClos
private final SdkTokenProvider tokenProvider;
private final RuntimeException loadException;

private final ProfileFile profileFile;
private final String profileName;

/**
* @see #builder()
*/
private ProfileTokenProvider(BuilderImpl builder) {
SdkTokenProvider tokenProvider = null;
RuntimeException loadException = null;
ProfileFile profileFile = null;
String profileName = null;
SdkTokenProvider sdkTokenProvider = null;
RuntimeException thrownException = null;
Supplier<ProfileFile> selectedProfileFile = null;
String selectedProfileName = null;

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

// Load the profiles file
profileFile = Optional.ofNullable(builder.profileFile)
.orElse(builder.defaultProfileFileLoader)
.get();
selectedProfileFile = Optional.ofNullable(builder.profileFile)
.orElse(builder.defaultProfileFileLoader);

// Load the profile and token provider
String finalProfileName = profileName;
ProfileFile finalProfileFile = profileFile;
tokenProvider =
profileFile.profile(profileName)
.flatMap(p -> new ProfileTokenProviderLoader(finalProfileFile, p).tokenProvider())
.orElseThrow(() -> {
String errorMessage = String.format("Profile file contained no information for " +
"profile '%s': %s", finalProfileName, finalProfileFile);
return SdkClientException.builder().message(errorMessage).build();
});
sdkTokenProvider = createTokenProvider(selectedProfileFile, selectedProfileName);

} catch (RuntimeException e) {
// If we couldn't load the provider for some reason, save an exception describing why.
loadException = e;
thrownException = e;
}

if (loadException != null) {
this.loadException = loadException;
if (thrownException != null) {
this.loadException = thrownException;
this.tokenProvider = null;
this.profileFile = null;
this.profileName = null;
} else {
this.loadException = null;
this.tokenProvider = tokenProvider;
this.profileFile = profileFile;
this.profileName = profileName;
this.tokenProvider = sdkTokenProvider;
this.profileName = selectedProfileName;
}
}

Expand Down Expand Up @@ -127,7 +115,6 @@ public SdkToken resolveToken() {
public String toString() {
return ToString.builder("ProfileTokenProvider")
.add("profileName", profileName)
.add("profileFile", profileFile)
.build();
}

Expand All @@ -137,6 +124,16 @@ public void close() {
IoUtils.closeIfCloseable(tokenProvider, null);
}

private SdkTokenProvider createTokenProvider(Supplier<ProfileFile> profileFile, String profileName) {
return new ProfileTokenProviderLoader(profileFile, profileName)
.tokenProvider()
.orElseThrow(() -> {
String errorMessage = String.format("Profile file contained no information for " +
"profile '%s'", profileName);
return SdkClientException.builder().message(errorMessage).build();
});
}

/**
* A builder for creating a custom {@link ProfileTokenProvider}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.token.credentials.ChildProfileTokenProviderFactory;
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.internal.util.ClassLoaderHelper;
import software.amazon.awssdk.profiles.Profile;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.profiles.internal.ProfileSection;
import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.Validate;

/**
Expand All @@ -36,12 +40,17 @@ public final class ProfileTokenProviderLoader {
private static final String SSO_OIDC_TOKEN_PROVIDER_FACTORY =
"software.amazon.awssdk.services.ssooidc.SsoOidcProfileTokenProviderFactory";

private final Profile profile;
private final ProfileFile profileFile;
private final Supplier<ProfileFile> profileFileSupplier;
private final String profileName;
private volatile ProfileFile currentProfileFile;
private volatile SdkTokenProvider currentTokenProvider;

public ProfileTokenProviderLoader(ProfileFile profileFile, Profile profile) {
this.profile = Validate.paramNotNull(profile, "profile");
this.profileFile = Validate.paramNotNull(profileFile, "profileFile");
private final Lazy<ChildProfileTokenProviderFactory> factory;

public ProfileTokenProviderLoader(Supplier<ProfileFile> profileFile, String profileName) {
this.profileFileSupplier = Validate.paramNotNull(profileFile, "profileFile");
this.profileName = Validate.paramNotNull(profileName, "profileName");
this.factory = new Lazy<>(this::ssoTokenProviderFactory);
}

/**
Expand All @@ -55,19 +64,53 @@ public Optional<SdkTokenProvider> tokenProvider() {
* Create the SSO credentials provider based on the related profile properties.
*/
private SdkTokenProvider ssoProfileCredentialsProvider() {
return () -> ssoProfileCredentialsProvider(profileFileSupplier, profileName).resolveToken();
}

private SdkTokenProvider ssoProfileCredentialsProvider(ProfileFile profileFile, Profile profile) {
String profileSsoSectionName = profileSsoSectionName(profile);
Profile ssoProfile = ssoProfile(profileFile, profileSsoSectionName);

validateRequiredProperties(ssoProfile, ProfileProperty.SSO_REGION, ProfileProperty.SSO_START_URL);

return factory.getValue().create(profileFile, profile);
}

String profileSsoSectionName = profile.property(ProfileSection.SSO_SESSION.getPropertyKeyName())
.orElseThrow(() -> new IllegalArgumentException(
"Profile " + profile.name() + " does not have sso_session property"));
private SdkTokenProvider ssoProfileCredentialsProvider(Supplier<ProfileFile> profileFile, String profileName) {
ProfileFile profileFileInstance = profileFile.get();
if (!Objects.equals(profileFileInstance, currentProfileFile)) {
synchronized (this) {
if (!Objects.equals(profileFileInstance, currentProfileFile)) {
Profile profileInstance = resolveProfile(profileFileInstance, profileName);
currentProfileFile = profileFileInstance;
currentTokenProvider = ssoProfileCredentialsProvider(profileFileInstance, profileInstance);
}
}
}

return currentTokenProvider;
}

Profile ssoProfile = profileFile.getSection(ProfileSection.SSO_SESSION.getSectionTitle(), profileSsoSectionName)
.orElseThrow(() -> new IllegalArgumentException(
"Sso-session section not found with sso-session title " + profileSsoSectionName));
private Profile resolveProfile(ProfileFile profileFile, String profileName) {
return profileFile.profile(profileName)
.orElseThrow(() -> {
String errorMessage = String.format("Profile file contained no information for profile '%s': %s",
profileName, profileFile);
return SdkClientException.builder().message(errorMessage).build();
});
}

private String profileSsoSectionName(Profile profile) {
return Optional.ofNullable(profile)
.flatMap(p -> p.property(ProfileSection.SSO_SESSION.getPropertyKeyName()))
.orElseThrow(() -> new IllegalArgumentException(
"Profile " + profileName + " does not have sso_session property"));
}

validateRequiredProperties(ssoProfile,
ProfileProperty.SSO_REGION,
ProfileProperty.SSO_START_URL);
return ssoTokenProviderFactory().create(profileFile, profile);
private Profile ssoProfile(ProfileFile profileFile, String profileSsoSectionName) {
return profileFile.getSection(ProfileSection.SSO_SESSION.getSectionTitle(), profileSsoSectionName)
.orElseThrow(() -> new IllegalArgumentException(
"Sso-session section not found with sso-session title " + profileSsoSectionName));
}

/**
Expand All @@ -76,7 +119,8 @@ private SdkTokenProvider ssoProfileCredentialsProvider() {
private void validateRequiredProperties(Profile ssoProfile, String... requiredProperties) {
Arrays.stream(requiredProperties)
.forEach(p -> Validate.isTrue(ssoProfile.properties().containsKey(p),
"Property '%s' was not configured for profile '%s'.", p, this.profile.name()));
"Property '%s' was not configured for profile '%s'.",
p, profileName));
}

/**
Expand All @@ -88,10 +132,10 @@ private ChildProfileTokenProviderFactory ssoTokenProviderFactory() {
getClass());
return (ChildProfileTokenProviderFactory) ssoOidcTokenProviderFactory.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalStateException("To use SSO OIDC related properties in the '" + profile.name() + "' profile, "
throw new IllegalStateException("To use SSO OIDC related properties in the '" + profileName + "' profile, "
+ "the 'ssooidc' service module must be on the class path.", e);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Failed to create the '" + profile.name() + "' token provider factory.", e);
throw new IllegalStateException("Failed to create the '%s" + profileName + "' token provider factory.", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.auth.token.credentials.ProfileTokenProvider;
import org.mockito.Mockito;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.utils.StringInputStream;

public class ProfileTokenProviderTest {
class ProfileTokenProviderTest {

@Test
public void missingProfileFile_throwsException() {
void missingProfileFile_throwsException() {
ProfileTokenProvider provider =
new ProfileTokenProvider.BuilderImpl()
.defaultProfileFileLoader(() -> ProfileFile.builder()
Expand All @@ -39,7 +40,7 @@ public void missingProfileFile_throwsException() {
}

@Test
public void emptyProfileFile_throwsException() {
void emptyProfileFile_throwsException() {
ProfileTokenProvider provider =
new ProfileTokenProvider.BuilderImpl()
.defaultProfileFileLoader(() -> ProfileFile.builder()
Expand All @@ -52,7 +53,7 @@ public void emptyProfileFile_throwsException() {
}

@Test
public void missingProfile_throwsException() {
void missingProfile_throwsException() {
ProfileFile file = profileFile("[default]\n"
+ "aws_access_key_id = defaultAccessKey\n"
+ "aws_secret_access_key = defaultSecretAccessKey");
Expand All @@ -64,7 +65,7 @@ public void missingProfile_throwsException() {
}

@Test
public void compatibleProfileSettings_callsLoader() {
void compatibleProfileSettings_callsLoader() {
ProfileFile file = profileFile("[default]");

ProfileTokenProvider provider =
Expand All @@ -73,6 +74,28 @@ public void compatibleProfileSettings_callsLoader() {
assertThatThrownBy(provider::resolveToken).hasMessageContaining("does not have sso_session property");
}

@Test
void resolveToken_profileFileSupplier_suppliesObjectPerCall() {
ProfileFile file1 = profileFile("[profile sso]\n"
+ "aws_access_key_id = defaultAccessKey\n"
+ "aws_secret_access_key = defaultSecretAccessKey\n"
+ "sso_session = xyz");
ProfileFile file2 = profileFile("[profile sso]\n"
+ "aws_access_key_id = modifiedAccessKey\n"
+ "aws_secret_access_key = modifiedSecretAccessKey\n"
+ "sso_session = xyz");
Supplier<ProfileFile> supplier = Mockito.mock(Supplier.class);

ProfileTokenProvider provider =
ProfileTokenProvider.builder().profileFile(supplier).profileName("sso").build();

Mockito.when(supplier.get()).thenReturn(file1, file2);
assertThatThrownBy(provider::resolveToken).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(provider::resolveToken).isInstanceOf(IllegalArgumentException.class);

Mockito.verify(supplier, Mockito.times(2)).get();
}

private ProfileFile profileFile(String string) {
return ProfileFile.builder()
.content(new StringInputStream(string))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static software.amazon.awssdk.services.ssooidc.internal.SsoOidcTokenProviderTest.START_URL;
import static software.amazon.awssdk.services.ssooidc.internal.SsoOidcTokenProviderTest.deriveCacheKey;
import static software.amazon.awssdk.utils.UserHomeDirectoryUtils.userHomeDirectory;

Expand All @@ -41,7 +40,6 @@
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider;
import software.amazon.awssdk.auth.token.internal.ProfileTokenProviderLoader;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.Profile;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.services.ssooidc.SsoOidcClient;
Expand All @@ -59,7 +57,7 @@ public abstract class SsoOidcTokenRefreshTestBase {
protected boolean shouldMockServiceClient;
protected SsoOidcClient ssoOidcClient;
protected String testStartUrl;
protected String testSessionName = "sso-prod";;
protected String testSessionName = "sso-prod";
protected String baseTokenResourceFile;
protected ProfileTokenProviderLoader profileTokenProviderLoader;

Expand Down Expand Up @@ -98,12 +96,11 @@ protected void initializeProfileProperties() {
"sso_region=us-east-1\n" +
"sso_start_url=" + testStartUrl + "\n";

ProfileFile profiles = ProfileFile.builder()
ProfileFile profile = ProfileFile.builder()
.content(new StringInputStream(profileContent))
.type(ProfileFile.Type.CONFIGURATION)
.build();
Optional<Profile> profile = profiles.profile("sso-refresh");
profileTokenProviderLoader = new ProfileTokenProviderLoader(profiles, profile.get());
profileTokenProviderLoader = new ProfileTokenProviderLoader(() -> profile, "sso-refresh");
}

@Test
Expand Down
Loading