diff --git a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/ChainSetup.java b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/ChainSetup.java index e8a1f79063..7c6c254e90 100644 --- a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/ChainSetup.java +++ b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/ChainSetup.java @@ -50,6 +50,7 @@ private ChainSetup(Builder builder) { this.profileNameOverride = builder.profileNameOverride; this.properties = Context.create(); this.envFn = builder.envFn; + this.profileFile = builder.profileFile; } /** @@ -214,6 +215,7 @@ public static final class Builder { private ScheduledExecutorService executor; private String profileNameOverride; private Function envFn = System::getenv; + private AwsProfileFile profileFile; private Builder() {} @@ -252,6 +254,19 @@ public Builder env(Function envFn) { return this; } + /** + * Supplies an already-parsed AWS config/credentials file. When set, the {@code SHARED_CONFIG} provider + * uses this file instead of reading {@code ~/.aws/config} and {@code ~/.aws/credentials} from disk. Use + * when the caller has already loaded the profile file, or to point the chain at a non-default location. + * + * @param profileFile the parsed profile file. + * @return this builder. + */ + public Builder profileFile(AwsProfileFile profileFile) { + this.profileFile = profileFile; + return this; + } + /** * Builds the {@link ChainSetup}. * diff --git a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java index 9465c16fa8..6a24c50f28 100644 --- a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java +++ b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/CredentialChain.java @@ -18,6 +18,7 @@ import software.amazon.smithy.java.auth.api.identity.Identity; import software.amazon.smithy.java.auth.api.identity.IdentityResolver; import software.amazon.smithy.java.auth.api.identity.IdentityResult; +import software.amazon.smithy.java.aws.config.AwsProfileFile; import software.amazon.smithy.java.client.core.CallContext; import software.amazon.smithy.java.context.Context; import software.amazon.smithy.java.logging.InternalLogger; @@ -81,11 +82,34 @@ public static CredentialChain create(Class identityTy * @throws IllegalStateException if two providers claim the same standard slot. */ public static CredentialChain create(Class identityType, ScheduledExecutorService ex) { + return create(identityType, ex, null); + } + + /** + * Create a credential chain by discovering providers via ServiceLoader, using a caller-supplied AWS + * config/credentials file. + * + *

When {@code profileFile} is non-null, the {@code SHARED_CONFIG} provider uses it instead of reading + * {@code ~/.aws/config} and {@code ~/.aws/credentials} from disk. Use this when the file has already been + * loaded, or to point the chain at a non-default location. + * + * @param identityType Identity type to resolve. + * @param ex Executor used for background resolution. + * @param profileFile Already-parsed profile file to use, or {@code null} to load from the default locations. + * @return the assembled chain. + * @throws IllegalStateException if two providers claim the same standard slot. + */ + public static CredentialChain create( + Class identityType, + ScheduledExecutorService ex, + AwsProfileFile profileFile + ) { List registrations = new ArrayList<>(); for (ChainIdentityProvider r : ServiceLoader.load(ChainIdentityProvider.class)) { registrations.add(r); } - return assemble(identityType, registrations, ex); + ChainSetup setup = ChainSetup.builder().executor(ex).profileFile(profileFile).build(); + return assemble(identityType, registrations, ex, setup); } static CredentialChain assemble( diff --git a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/config/SharedConfigProvider.java b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/config/SharedConfigProvider.java index 6f07dec46a..2bd70e0233 100644 --- a/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/config/SharedConfigProvider.java +++ b/aws/aws-credential-chain/src/main/java/software/amazon/smithy/java/aws/credentials/chain/config/SharedConfigProvider.java @@ -31,7 +31,13 @@ public OrderingConstraint ordering() { @Override public void setup(Class identityType, ChainSetup setup) { - AwsProfileFile profileFile = AwsProfileFile.loadSilently(); + // Defer to a profile file already supplied on the setup (e.g., injected by the client builder or an + // upstream provider) instead of reading from disk. Only fall back to loading the shared config/credentials + // files when none was provided. + AwsProfileFile profileFile = setup.profileFile(); + if (profileFile == null) { + profileFile = AwsProfileFile.loadSilently(); + } if (profileFile != null) { setup.setProfileFile(profileFile); String name = setup.profileNameOverride(); diff --git a/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/InjectedProfileFileTest.java b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/InjectedProfileFileTest.java new file mode 100644 index 0000000000..c3ac850b34 --- /dev/null +++ b/aws/aws-credential-chain/src/test/java/software/amazon/smithy/java/aws/credentials/chain/InjectedProfileFileTest.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.aws.credentials.chain; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.smithy.java.aws.config.AwsProfileFile; +import software.amazon.smithy.java.aws.credentials.chain.config.SharedConfigProvider; + +/** + * Verifies that an already-parsed {@link AwsProfileFile} supplied on the {@link ChainSetup} is used as-is and that + * {@link SharedConfigProvider} does not overwrite it by reading from disk. + */ +class InjectedProfileFileTest { + + @Test + void sharedConfigProviderUsesInjectedProfileFile(@TempDir Path tmp) throws IOException { + Path config = tmp.resolve("config"); + Files.writeString(config, """ + [default] + aws_access_key_id = injected_ak + aws_secret_access_key = injected_sk + """); + var injected = AwsProfileFile.builder().configFile(config).credentialsFile(null).build(); + + // Empty environment so the active profile resolves to "default" deterministically, regardless of any + // AWS_PROFILE set in the ambient test environment. + var setup = ChainSetup.builder().profileFile(injected).env(k -> null).build(); + var provider = new SharedConfigProvider(); + setup.setCurrentProvider(provider); + provider.setup(software.amazon.smithy.java.aws.auth.api.identity.AwsCredentialsIdentity.class, setup); + + // The injected instance survives: SharedConfigProvider did not replace it with a disk load. + assertSame(injected, + setup.profileFile(), + "SharedConfigProvider should defer to the profile file already set on the chain setup."); + // And it resolved the active profile from that injected file. + assertNotNull(setup.profile()); + } +}