diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java index 698955eb84e5..bd15f28ff2e6 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java @@ -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 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; } } @@ -127,7 +115,6 @@ public SdkToken resolveToken() { public String toString() { return ToString.builder("ProfileTokenProvider") .add("profileName", profileName) - .add("profileFile", profileFile) .build(); } @@ -137,6 +124,16 @@ public void close() { IoUtils.closeIfCloseable(tokenProvider, null); } + private SdkTokenProvider createTokenProvider(Supplier 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}. */ diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java b/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java index 87e00046947e..4545477d9fce 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java @@ -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; /** @@ -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 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 factory; + + public ProfileTokenProviderLoader(Supplier profileFile, String profileName) { + this.profileFileSupplier = Validate.paramNotNull(profileFile, "profileFile"); + this.profileName = Validate.paramNotNull(profileName, "profileName"); + this.factory = new Lazy<>(this::ssoTokenProviderFactory); } /** @@ -55,19 +64,53 @@ public Optional 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, 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)); } /** @@ -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)); } /** @@ -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); } } } diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java index a03cd87d7cf4..f5324dd43771 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java @@ -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() @@ -39,7 +40,7 @@ public void missingProfileFile_throwsException() { } @Test - public void emptyProfileFile_throwsException() { + void emptyProfileFile_throwsException() { ProfileTokenProvider provider = new ProfileTokenProvider.BuilderImpl() .defaultProfileFileLoader(() -> ProfileFile.builder() @@ -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"); @@ -64,7 +65,7 @@ public void missingProfile_throwsException() { } @Test - public void compatibleProfileSettings_callsLoader() { + void compatibleProfileSettings_callsLoader() { ProfileFile file = profileFile("[default]"); ProfileTokenProvider provider = @@ -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 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)) diff --git a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java index d925d448b4f1..4d43bd154426 100644 --- a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java +++ b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java @@ -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; @@ -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; @@ -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; @@ -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 = profiles.profile("sso-refresh"); - profileTokenProviderLoader = new ProfileTokenProviderLoader(profiles, profile.get()); + profileTokenProviderLoader = new ProfileTokenProviderLoader(() -> profile, "sso-refresh"); } @Test diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java index 7a7540c8081e..be3e93eef82c 100644 --- a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java @@ -17,10 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.HashMap; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,45 +30,50 @@ 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.utils.StringInputStream; -public class ProfileTokenProviderLoaderTest { +class ProfileTokenProviderLoaderTest { @Test - public void noProfile_throwsException() { - assertThatThrownBy(() -> new ProfileTokenProviderLoader(ProfileFile.defaultProfileFile(), null)) - .hasMessageContaining("profile must not be null"); + void profileTokenProviderLoader_noProfileFileSupplier_throwsException() { + assertThatThrownBy(() -> new ProfileTokenProviderLoader(null, "sso'")) + .hasMessageContaining("profileFile must not be null"); } @Test - public void noProfileFile_throwsException() { - assertThatThrownBy(() -> new ProfileTokenProviderLoader(null, Profile.builder().name("sso").properties(new HashMap<>()).build())) - .hasMessageContaining("profileFile must not be null"); + void profileTokenProviderLoader_noProfileName_throwsException() { + assertThatThrownBy(() -> new ProfileTokenProviderLoader(ProfileFile::defaultProfileFile, null)) + .hasMessageContaining("profileName must not be null"); } @ParameterizedTest @MethodSource("ssoErrorValues") - public void incorrectSsoProperties_throwsException(String profileContent, String msg) { + void incorrectSsoProperties_supplier_delaysThrowingExceptionUntilResolvingToken(String profileContent, String msg) { ProfileFile profileFile = configFile(profileContent); + Supplier supplier = () -> profileFile; + + ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(supplier, "sso"); + + assertThatNoException().isThrownBy(providerLoader::tokenProvider); - assertThat(profileFile.profile("sso")).hasValueSatisfying(profile -> { - ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(profileFile, profile); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(providerLoader::tokenProvider).withMessageContaining(msg); + assertThat(providerLoader.tokenProvider()).satisfies(tokenProviderOptional -> { + assertThat(tokenProviderOptional).isPresent().get().satisfies(tokenProvider-> { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(tokenProvider::resolveToken) + .withMessageContaining(msg); + }); }); } @Test - public void correctSsoProperties_createsTokenProvider() { + void correctSsoProperties_createsTokenProvider() { String profileContent = "[profile sso]\n" + "sso_session=admin\n" + "[sso-session admin]\n" + "sso_region=us-east-1\n" + "sso_start_url=https://d-abc123.awsapps.com/start\n"; - Optional ssoProfile = configFile(profileContent).profile("sso"); - ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(configFile(profileContent), ssoProfile.get()); + ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(() -> configFile(profileContent), "sso"); Optional tokenProvider = providerLoader.tokenProvider(); assertThat(tokenProvider).isPresent(); assertThatThrownBy(() -> tokenProvider.get().resolveToken())