diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts index 268b93ddf3056..dc1c5df8c8ce8 100644 --- a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -30,7 +30,11 @@ export class AwsCliCompatible { * 3. Respects $AWS_SHARED_CREDENTIALS_FILE. * 4. Respects $AWS_DEFAULT_PROFILE in addition to $AWS_PROFILE. */ - public static async credentialChain(profile: string | undefined, ec2creds: boolean | undefined, containerCreds: boolean | undefined) { + public static async credentialChain( + profile: string | undefined, + ec2creds: boolean | undefined, + containerCreds: boolean | undefined, + httpOptions: AWS.HTTPOptions | undefined) { await forceSdkToReadConfigIfPresent(); profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; @@ -41,11 +45,11 @@ export class AwsCliCompatible { ]; if (await fs.pathExists(credentialsFileName())) { - sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() })); + sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions })); } if (await fs.pathExists(configFileName())) { - sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() })); + sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions })); } if (containerCreds ?? hasEcsCredentials()) { diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 52523d1e52a50..d0b977225824a 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -88,14 +88,15 @@ export class SdkProvider { * class `AwsCliCompatible` for the details. */ public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) { - const chain = await AwsCliCompatible.credentialChain(options.profile, options.ec2creds, options.containerCreds); + const sdkOptions = parseHttpOptions(options.httpOptions ?? {}); + + const chain = await AwsCliCompatible.credentialChain(options.profile, options.ec2creds, options.containerCreds, sdkOptions.httpOptions); const region = await AwsCliCompatible.region(options.profile); - return new SdkProvider(chain, region, options.httpOptions); + return new SdkProvider(chain, region, sdkOptions); } private readonly plugins = new CredentialPlugins(); - private readonly httpOptions: ConfigurationOptions; public constructor( private readonly defaultChain: AWS.CredentialProviderChain, @@ -103,8 +104,7 @@ export class SdkProvider { * Default region */ public readonly defaultRegion: string, - httpOptions: SdkHttpOptions = {}) { - this.httpOptions = defaultHttpOptions(httpOptions); + private readonly sdkOptions: ConfigurationOptions = {}) { } /** @@ -116,7 +116,7 @@ export class SdkProvider { public async forEnvironment(accountId: string | undefined, region: string | undefined, mode: Mode): Promise { const env = await this.resolveEnvironment(accountId, region); const creds = await this.obtainCredentials(env.account, mode); - return new SDK(creds, env.region, this.httpOptions); + return new SDK(creds, env.region, this.sdkOptions); } /** @@ -139,12 +139,12 @@ export class SdkProvider { }, stsConfig: { region, - ...this.httpOptions, + ...this.sdkOptions, }, masterCredentials: await this.defaultCredentials(), }); - return new SDK(creds, region, this.httpOptions); + return new SDK(creds, region, this.sdkOptions); } /** @@ -199,7 +199,7 @@ export class SdkProvider { throw new Error('Unable to resolve AWS credentials (setup with "aws configure")'); } - return new SDK(creds, this.defaultRegion, this.httpOptions).currentAccount(); + return new SDK(creds, this.defaultRegion, this.sdkOptions).currentAccount(); } catch (e) { debug('Unable to determine the default AWS account:', e); return undefined; @@ -269,8 +269,11 @@ export interface Account { * Get HTTP options for the SDK * * Read from user input or environment variables. + * + * Returns a complete `ConfigurationOptions` object because that's where + * `customUserAgent` lives, but `httpOptions` is the most important attribute. */ -function defaultHttpOptions(options: SdkHttpOptions) { +function parseHttpOptions(options: SdkHttpOptions) { const config: ConfigurationOptions = {}; config.httpOptions = {}; diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 1ae9e099d209d..e6ea2f0048837 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -34,6 +34,10 @@ beforeEach(() => { [foo] aws_access_key_id=${uid}fooccess aws_secret_access_key=secret + + [assumer] + aws_access_key_id=${uid}assumer + aws_secret_access_key=secret `), '/home/me/.bxt/config': dedent(` [default] @@ -46,6 +50,13 @@ beforeEach(() => { aws_access_key_id=${uid}booccess aws_secret_access_key=boocret # No region here + + [profile assumable] + role_arn=arn:aws:iam::12356789012:role/Assumable + source_profile=assumer + + [profile assumer] + region=us-east-2 `), }); @@ -138,6 +149,43 @@ describe('CLI compatible credentials loading', () => { await expect(provider.forEnvironment(`${uid}some_account_#`, 'def', Mode.ForReading)).rejects.toThrow('Need to perform AWS calls'); }); + + test('even when using a profile to assume another profile, STS calls goes through the proxy', async () => { + // Messy mocking + let called = false; + jest.mock('proxy-agent', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + class FakeAgent extends require('https').Agent { + public addRequest(_: any, __: any) { + // FIXME: this error takes 6 seconds to be completely handled. It + // might be retries in the SDK somewhere, or something about the Node + // event loop. I've spent an hour trying to figure it out and I can't, + // and I gave up. We'll just have to live with this until someone gets + // inspired. + const error = new Error('ABORTED BY TEST'); + (error as any).code = 'RequestAbortedError'; + (error as any).retryable = false; + called = true; + throw error; + } + } + return FakeAgent; + }); + + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, + ec2creds: false, + profile: 'assumable', + httpOptions: { + proxyAddress: 'http://DOESNTMATTER/', + } + }); + + await provider.defaultAccount(); + + // THEN -- the fake proxy agent got called, we don't care about the result + expect(called).toEqual(true); + }); }); describe('Plugins', () => { diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index efc10b2d0ec66..1c1cbcf29506d 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -12,7 +12,7 @@ export class MockSdkProvider extends SdkProvider { private readonly sdk: ISDK; constructor() { - super(new AWS.CredentialProviderChain([]), 'bermuda-triangle-1337', { userAgent: 'aws-cdk/jest' }); + super(new AWS.CredentialProviderChain([]), 'bermuda-triangle-1337', { customUserAgent: 'aws-cdk/jest' }); // SDK contains a real SDK, since some test use 'AWS-mock' to replace the underlying // AWS calls which a real SDK would do, and some tests use the 'stub' functionality below.