Skip to content

Commit 95c0332

Browse files
authored
fix(cli): AssumeRole profiles require a [default] profile (#10032)
This works around a bug in the AWS SDK for JS that only surfaced when we switched to `AWS_STS_REGIONAL_ENDPOINTS=regional`, requiring a `[default]` profile with a region for all users. The bug was that the INI-file AssumeRole provider would ignore the region in the profile, and always fall back to the region in: * The profile specified using `$AWS_PROFILE` (which we don't use). * Otherwise the region in the `[default]` profile (which a user may or may not have). Traditionally it didn't really matter whether the STS client got a region or not because it would always connect to `us-east-1` no matter what, but when we switched to `AWS_STS_REGIONAL_ENDPOINTS=regional`, it became illegal to not have a region. Fix the upstream bug by basically replicating the important parts of `SharedIniFileCredentials` of the AWS SDK in our codebase and patching the bug. Reported upstreeam as aws/aws-sdk-js#3418 Fixes #9937 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 9098e29 commit 95c0332

File tree

3 files changed

+380
-163
lines changed

3 files changed

+380
-163
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import * as AWS from 'aws-sdk';
2+
3+
4+
/**
5+
* Hack-fix
6+
*
7+
* There are a number of issues in the upstream version of SharedIniFileCredentials
8+
* that need fixing:
9+
*
10+
* 1. The upstream aws-sdk contains an incorrect instantiation of an `AWS.STS`
11+
* client, which *should* have taken the region from the requested profile
12+
* but doesn't. It will use the region from the default profile, which
13+
* may not exist, defaulting to `us-east-1` (since we switched to
14+
* AWS_STS_REGIONAL_ENDPOINTS=regional, that default is not even allowed anymore
15+
* and the absence of a default region will lead to an error).
16+
*
17+
* 2. The simple fix is to get the region from the `config` file. profiles
18+
* are made up of a combination of `credentials` and `config`, and the region is
19+
* generally in `config` with the rest in `credentials`. However, a bug in
20+
* `getProfilesFromSharedConfig` overwrites ALL `config` data with `credentials`
21+
* data, so we also need to do extra work to fish the `region` out of the config.
22+
*
23+
* See https://github.com/aws/aws-sdk-js/issues/3418 for all the gory details.
24+
*/
25+
export class PatchedSharedIniFileCredentials extends AWS.SharedIniFileCredentials {
26+
declare private profile: string;
27+
declare private filename: string;
28+
declare private disableAssumeRole: boolean;
29+
declare private options: Record<string, string>;
30+
declare private roleArn: string;
31+
declare private httpOptions?: AWS.HTTPOptions;
32+
declare private tokenCodeFn?: (mfaSerial: string, callback: (err?: Error, token?: string) => void) => void;
33+
34+
public loadRoleProfile(
35+
creds: Record<string, Record<string, string>>,
36+
roleProfile: Record<string, string>,
37+
callback: (err?: Error, data?: any) => void) {
38+
39+
// Need to duplicate the whole implementation here -- the function is long and has been written in
40+
// such a way that there are no small monkey patches possible.
41+
42+
if (this.disableAssumeRole) {
43+
throw (AWS as any).util.error(
44+
new Error('Role assumption profiles are disabled. ' +
45+
'Failed to load profile ' + this.profile +
46+
' from ' + creds.filename),
47+
{ code: 'SharedIniFileCredentialsProviderFailure' },
48+
);
49+
}
50+
51+
var self = this;
52+
var roleArn = roleProfile.role_arn;
53+
var roleSessionName = roleProfile.role_session_name;
54+
var externalId = roleProfile.external_id;
55+
var mfaSerial = roleProfile.mfa_serial;
56+
var sourceProfileName = roleProfile.source_profile;
57+
58+
if (!sourceProfileName) {
59+
throw (AWS as any).util.error(
60+
new Error('source_profile is not set using profile ' + this.profile),
61+
{ code: 'SharedIniFileCredentialsProviderFailure' },
62+
);
63+
}
64+
65+
var sourceProfileExistanceTest = creds[sourceProfileName];
66+
67+
if (typeof sourceProfileExistanceTest !== 'object') {
68+
throw (AWS as any).util.error(
69+
new Error('source_profile ' + sourceProfileName + ' using profile '
70+
+ this.profile + ' does not exist'),
71+
{ code: 'SharedIniFileCredentialsProviderFailure' },
72+
);
73+
}
74+
75+
var sourceCredentials = new AWS.SharedIniFileCredentials(
76+
(AWS as any).util.merge(this.options || {}, {
77+
profile: sourceProfileName,
78+
preferStaticCredentials: true,
79+
}),
80+
);
81+
82+
// --------- THIS IS NEW ----------------------
83+
const profiles = loadProfilesProper(this.filename);
84+
const region = profiles[this.profile]?.region ?? profiles.default?.region ?? 'us-east-1';
85+
// --------- /THIS IS NEW ----------------------
86+
87+
this.roleArn = roleArn;
88+
var sts = new AWS.STS({
89+
credentials: sourceCredentials,
90+
region,
91+
httpOptions: this.httpOptions,
92+
});
93+
94+
var roleParams: AWS.STS.AssumeRoleRequest = {
95+
RoleArn: roleArn,
96+
RoleSessionName: roleSessionName || 'aws-sdk-js-' + Date.now(),
97+
};
98+
99+
if (externalId) {
100+
roleParams.ExternalId = externalId;
101+
}
102+
103+
if (mfaSerial && self.tokenCodeFn) {
104+
roleParams.SerialNumber = mfaSerial;
105+
self.tokenCodeFn(mfaSerial, function(err, token) {
106+
if (err) {
107+
var message;
108+
if (err instanceof Error) {
109+
message = err.message;
110+
} else {
111+
message = err;
112+
}
113+
callback(
114+
(AWS as any).util.error(
115+
new Error('Error fetching MFA token: ' + message),
116+
{ code: 'SharedIniFileCredentialsProviderFailure' },
117+
));
118+
return;
119+
}
120+
121+
roleParams.TokenCode = token;
122+
sts.assumeRole(roleParams, callback);
123+
});
124+
return;
125+
}
126+
sts.assumeRole(roleParams, callback);
127+
128+
}
129+
}
130+
131+
/**
132+
* A function to load profiles from disk that MERGES credentials and config instead of overwriting
133+
*
134+
* @see https://github.com/aws/aws-sdk-js/blob/5ae5a7d7d24d1000dbc089cc15f8ed2c7b06c542/lib/util.js#L956
135+
*/
136+
function loadProfilesProper(filename: string) {
137+
const util = (AWS as any).util; // Does exists even though there aren't any typings for it
138+
const iniLoader = util.iniLoader;
139+
const profiles: Record<string, Record<string, string>> = {};
140+
let profilesFromConfig: Record<string, Record<string, string>> = {};
141+
if (process.env[util.configOptInEnv]) {
142+
profilesFromConfig = iniLoader.loadFrom({
143+
isConfig: true,
144+
filename: process.env[util.sharedConfigFileEnv],
145+
});
146+
}
147+
var profilesFromCreds: Record<string, Record<string, string>> = iniLoader.loadFrom({
148+
filename: filename ||
149+
(process.env[util.configOptInEnv] && process.env[util.sharedCredentialsFileEnv]),
150+
});
151+
for (const [name, profile] of Object.entries(profilesFromConfig)) {
152+
profiles[name] = profile;
153+
}
154+
for (const [name, profile] of Object.entries(profilesFromCreds)) {
155+
profiles[name] = {
156+
...profiles[name],
157+
...profile,
158+
};
159+
}
160+
return profiles;
161+
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as AWS from 'aws-sdk';
66
import * as fs from 'fs-extra';
77
import * as promptly from 'promptly';
88
import { debug } from '../../logging';
9+
import { PatchedSharedIniFileCredentials } from './aws-sdk-inifile';
910
import { SharedIniFile } from './sdk_ini_file';
1011

1112
/**
@@ -44,7 +45,7 @@ export class AwsCliCompatible {
4445
// Force reading the `config` file if it exists by setting the appropriate
4546
// environment variable.
4647
await forceSdkToReadConfigIfPresent();
47-
sources.push(() => new AWS.SharedIniFileCredentials({
48+
sources.push(() => new PatchedSharedIniFileCredentials({
4849
profile,
4950
filename: credentialsFileName(),
5051
httpOptions: options.httpOptions,
@@ -310,3 +311,4 @@ async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string)
310311
cb(err);
311312
}
312313
}
314+

0 commit comments

Comments
 (0)