Skip to content

Commit 18d83aa

Browse files
committed
fix(cli): profile AssumeRole credentials don't work via proxy
The proxy configuration was not being properly passed to the `AWS.SharedIniFileCredentials` object, so if users configure AssumeRole credentials in their `~/.aws/config` file, it would try to connect to STS directly instead of going through the proxy. Properly parse and pass the HTTP options to the right AWS file. (Note that this object only takes `HTTPOptions` and not `ConfigurationOptions`, so there's no way to override the user agent on this initial STS call). Fixes #7265.
1 parent 8c6fc43 commit 18d83aa

File tree

4 files changed

+67
-14
lines changed

4 files changed

+67
-14
lines changed

packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export class AwsCliCompatible {
3030
* 3. Respects $AWS_SHARED_CREDENTIALS_FILE.
3131
* 4. Respects $AWS_DEFAULT_PROFILE in addition to $AWS_PROFILE.
3232
*/
33-
public static async credentialChain(profile: string | undefined, ec2creds: boolean | undefined, containerCreds: boolean | undefined) {
33+
public static async credentialChain(
34+
profile: string | undefined,
35+
ec2creds: boolean | undefined,
36+
containerCreds: boolean | undefined,
37+
httpOptions: AWS.HTTPOptions | undefined) {
3438
await forceSdkToReadConfigIfPresent();
3539

3640
profile = profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default';
@@ -41,11 +45,11 @@ export class AwsCliCompatible {
4145
];
4246

4347
if (await fs.pathExists(credentialsFileName())) {
44-
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() }));
48+
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
4549
}
4650

4751
if (await fs.pathExists(configFileName())) {
48-
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName() }));
52+
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
4953
}
5054

5155
if (containerCreds ?? hasEcsCredentials()) {

packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,23 @@ export class SdkProvider {
8888
* class `AwsCliCompatible` for the details.
8989
*/
9090
public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) {
91-
const chain = await AwsCliCompatible.credentialChain(options.profile, options.ec2creds, options.containerCreds);
91+
const sdkOptions = parseHttpOptions(options.httpOptions ?? {});
92+
93+
const chain = await AwsCliCompatible.credentialChain(options.profile, options.ec2creds, options.containerCreds, sdkOptions.httpOptions);
9294
const region = await AwsCliCompatible.region(options.profile);
9395

94-
return new SdkProvider(chain, region, options.httpOptions);
96+
return new SdkProvider(chain, region, sdkOptions);
9597
}
9698

9799
private readonly plugins = new CredentialPlugins();
98-
private readonly httpOptions: ConfigurationOptions;
99100

100101
public constructor(
101102
private readonly defaultChain: AWS.CredentialProviderChain,
102103
/**
103104
* Default region
104105
*/
105106
public readonly defaultRegion: string,
106-
httpOptions: SdkHttpOptions = {}) {
107-
this.httpOptions = defaultHttpOptions(httpOptions);
107+
private readonly sdkOptions: ConfigurationOptions = {}) {
108108
}
109109

110110
/**
@@ -116,7 +116,7 @@ export class SdkProvider {
116116
public async forEnvironment(accountId: string | undefined, region: string | undefined, mode: Mode): Promise<ISDK> {
117117
const env = await this.resolveEnvironment(accountId, region);
118118
const creds = await this.obtainCredentials(env.account, mode);
119-
return new SDK(creds, env.region, this.httpOptions);
119+
return new SDK(creds, env.region, this.sdkOptions);
120120
}
121121

122122
/**
@@ -139,12 +139,12 @@ export class SdkProvider {
139139
},
140140
stsConfig: {
141141
region,
142-
...this.httpOptions,
142+
...this.sdkOptions,
143143
},
144144
masterCredentials: await this.defaultCredentials(),
145145
});
146146

147-
return new SDK(creds, region, this.httpOptions);
147+
return new SDK(creds, region, this.sdkOptions);
148148
}
149149

150150
/**
@@ -199,7 +199,7 @@ export class SdkProvider {
199199
throw new Error('Unable to resolve AWS credentials (setup with "aws configure")');
200200
}
201201

202-
return new SDK(creds, this.defaultRegion, this.httpOptions).currentAccount();
202+
return new SDK(creds, this.defaultRegion, this.sdkOptions).currentAccount();
203203
} catch (e) {
204204
debug('Unable to determine the default AWS account:', e);
205205
return undefined;
@@ -269,8 +269,11 @@ export interface Account {
269269
* Get HTTP options for the SDK
270270
*
271271
* Read from user input or environment variables.
272+
*
273+
* Returns a complete `ConfigurationOptions` object because that's where
274+
* `customUserAgent` lives, but `httpOptions` is the most important attribute.
272275
*/
273-
function defaultHttpOptions(options: SdkHttpOptions) {
276+
function parseHttpOptions(options: SdkHttpOptions) {
274277
const config: ConfigurationOptions = {};
275278
config.httpOptions = {};
276279

@@ -298,6 +301,7 @@ function defaultHttpOptions(options: SdkHttpOptions) {
298301
debug('Using proxy server: %s', proxyAddress);
299302
// eslint-disable-next-line @typescript-eslint/no-require-imports
300303
const ProxyAgent: any = require('proxy-agent');
304+
// tslint:disable-next-line:no-console
301305
config.httpOptions.agent = new ProxyAgent(proxyAddress);
302306
}
303307
if (caBundlePath) {

packages/aws-cdk/test/api/sdk-provider.test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ beforeEach(() => {
3434
[foo]
3535
aws_access_key_id=${uid}fooccess
3636
aws_secret_access_key=secret
37+
38+
[assumer]
39+
aws_access_key_id=${uid}assumer
40+
aws_secret_access_key=secret
3741
`),
3842
'/home/me/.bxt/config': dedent(`
3943
[default]
@@ -46,6 +50,13 @@ beforeEach(() => {
4650
aws_access_key_id=${uid}booccess
4751
aws_secret_access_key=boocret
4852
# No region here
53+
54+
[profile assumable]
55+
role_arn=arn:aws:iam::12356789012:role/Assumable
56+
source_profile=assumer
57+
58+
[profile assumer]
59+
region=us-east-2
4960
`),
5061
});
5162

@@ -138,6 +149,40 @@ describe('CLI compatible credentials loading', () => {
138149

139150
await expect(provider.forEnvironment(`${uid}some_account_#`, 'def', Mode.ForReading)).rejects.toThrow('Need to perform AWS calls');
140151
});
152+
153+
test('even when using a profile to assume another profile, STS calls goes through the proxy', async () => {
154+
// Messy mocking
155+
let called = false;
156+
jest.mock('proxy-agent', () => {
157+
class FakeAgent extends require('https').Agent {
158+
public addRequest(_: any, __: any) {
159+
// This error takes 6 seconds to be completely handled. It might be retries in the SDK
160+
// somewhere, or something about the Node event loop. I've spent an hour trying to figure
161+
// it out and I can't, and I gave up. We'll just have to live with this.
162+
const error = new Error('ABORTED BY TEST');
163+
(error as any).code = 'RequestAbortedError';
164+
(error as any).retryable = false;
165+
called = true;
166+
throw error;
167+
}
168+
}
169+
return FakeAgent;
170+
});
171+
172+
// WHEN
173+
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions,
174+
ec2creds: false,
175+
profile: 'assumable',
176+
httpOptions: {
177+
proxyAddress: 'http://DOESNTMATTER/',
178+
}
179+
});
180+
181+
await provider.defaultAccount();
182+
183+
// THEN -- the fake proxy agent got called, we don't care about the result
184+
expect(called).toEqual(true);
185+
});
141186
});
142187

143188
describe('Plugins', () => {

packages/aws-cdk/test/util/mock-sdk.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class MockSdkProvider extends SdkProvider {
1212
private readonly sdk: ISDK;
1313

1414
constructor() {
15-
super(new AWS.CredentialProviderChain([]), 'bermuda-triangle-1337', { userAgent: 'aws-cdk/jest' });
15+
super(new AWS.CredentialProviderChain([]), 'bermuda-triangle-1337', { customUserAgent: 'aws-cdk/jest' });
1616

1717
// SDK contains a real SDK, since some test use 'AWS-mock' to replace the underlying
1818
// AWS calls which a real SDK would do, and some tests use the 'stub' functionality below.

0 commit comments

Comments
 (0)