Skip to content

Commit c9590c7

Browse files
trivikrJordonPhillips
authored andcommitted
feat(credential-provider-ini): call fromTokenFile in assumeRole chaining (#2178)
1 parent 04eba42 commit c9590c7

File tree

4 files changed

+176
-7
lines changed

4 files changed

+176
-7
lines changed

packages/credential-provider-ini/README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55

66
## AWS Credential Provider for Node.JS - Shared Configuration Files
77

8-
This module provides a function, `fromSharedConfigFiles` that will create
8+
This module provides a function, `fromIni` that will create
99
`CredentialProvider` functions that read from a shared credentials file at
1010
`~/.aws/credentials` and a shared configuration file at `~/.aws/config`. Both
1111
files are expected to be INI formatted with section names corresponding to
1212
profiles. Sections in the credentials file are treated as profile names, whereas
13-
profile sections in the config file must have the format of`[profile profile-name]`, except for the default profile. Please see the [sample
13+
profile sections in the config file must have the format of`[profile profile-name]`,
14+
except for the default profile. Please see the [sample
1415
files](#sample-files) below for examples of well-formed configuration and
1516
credentials files.
1617

@@ -21,8 +22,7 @@ in the config file.
2122
## Supported configuration
2223

2324
You may customize how credentials are resolved by providing an options hash to
24-
the `fromSharedConfigFiles` factory function. The following options are
25-
supported:
25+
the `fromIni` factory function. The following options are supported:
2626

2727
- `profile` - The configuration profile to use. If not specified, the provider
2828
will use the value in the `AWS_PROFILE` environment variable or a default of
@@ -38,7 +38,11 @@ supported:
3838
code and `mfaCodeProvider` is not a valid function, the credential provider
3939
promise will be rejected.
4040
- `roleAssumer` - A function that assumes a role and returns a promise
41-
fulfilled with credentials for the assumed role.
41+
fulfilled with credentials for the assumed role. You may call `sts:assumeRole`
42+
API within this function.
43+
- `roleAssumerWithWebIdentity` - A function that assumes a role with web identity
44+
and returns a promise fulfilled with credentials for the assumed role. You may call
45+
`sts:assumeRoleWithWebIdentity` API within this function.
4246

4347
## Sample files
4448

@@ -77,3 +81,23 @@ aws_secret_access_key=bar3
7781
aws_access_key_id=foo4
7882
aws_secret_access_key=bar4
7983
```
84+
85+
### source profile with static credentials
86+
87+
```ini
88+
[second]
89+
aws_access_key_id=foo
90+
aws_secret_access_key=bar
91+
92+
[first]
93+
source_profile=first
94+
role_arn=arn:aws:iam::123456789012:role/example-role-arn
95+
```
96+
97+
### profile with web_identity_token_file
98+
99+
```ini
100+
[default]
101+
web_identity_token_file=/temp/token
102+
role_arn=arn:aws:iam::123456789012:role/example-role-arn
103+
```

packages/credential-provider-ini/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"license": "Apache-2.0",
2424
"dependencies": {
25+
"@aws-sdk/credential-provider-web-identity": "3.0.0",
2526
"@aws-sdk/property-provider": "3.8.0",
2627
"@aws-sdk/shared-ini-file-loader": "3.8.0",
2728
"@aws-sdk/types": "3.6.1",

packages/credential-provider-ini/src/index.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
12
import { ENV_CONFIG_PATH, ENV_CREDENTIALS_PATH } from "@aws-sdk/shared-ini-file-loader";
23
import { Credentials } from "@aws-sdk/types";
34
import { join, sep } from "path";
@@ -48,8 +49,11 @@ jest.mock("os", () => {
4849

4950
return os;
5051
});
52+
5153
import { homedir } from "os";
5254

55+
jest.mock("@aws-sdk/credential-provider-web-identity");
56+
5357
const DEFAULT_CREDS = {
5458
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
5559
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
@@ -83,6 +87,7 @@ const envAtLoadTime: { [key: string]: string | undefined } = [
8387

8488
beforeEach(() => {
8589
__clearMatchers();
90+
jest.clearAllMocks();
8691
Object.keys(envAtLoadTime).forEach((envKey) => {
8792
delete process.env[envKey];
8893
});
@@ -749,6 +754,109 @@ source_profile = default`.trim()
749754
});
750755
});
751756

757+
describe("assume role with web identity", () => {
758+
it("should call fromTokenFile with data from profile", async () => {
759+
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
760+
const webIdentityTokenFile = "/temp/foo/token";
761+
const roleArn = "arn:aws:iam::123456789:role/bar";
762+
const roleSessionName = "bazSession";
763+
const roleAssumerWithWebIdentity = jest.fn();
764+
__addMatcher(
765+
join(homedir(), ".aws", "credentials"),
766+
`
767+
[foo]
768+
web_identity_token_file = ${webIdentityTokenFile}
769+
role_arn = ${roleArn}
770+
role_session_name = ${roleSessionName}`.trim()
771+
);
772+
773+
const provider = fromIni({
774+
profile: "foo",
775+
roleAssumerWithWebIdentity,
776+
});
777+
778+
expect(await provider()).toEqual(FOO_CREDS);
779+
expect(fromTokenFile).toHaveBeenCalledTimes(1);
780+
expect(fromTokenFile).toHaveBeenCalledWith({
781+
webIdentityTokenFile,
782+
roleArn,
783+
roleSessionName,
784+
roleAssumerWithWebIdentity,
785+
});
786+
});
787+
788+
it("should call fromTokenFile with assume role chaining", async () => {
789+
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(DEFAULT_CREDS));
790+
const webIdentityTokenFile = "/temp/foo/token";
791+
const roleArn = "arn:aws:iam::123456789:role/bar";
792+
const roleSessionName = "bazSession";
793+
const roleAssumerWithWebIdentity = jest.fn();
794+
795+
const fooRoleArn = "arn:aws:iam::123456789:role/foo";
796+
const fooSessionName = "fooSession";
797+
__addMatcher(
798+
join(homedir(), ".aws", "credentials"),
799+
`
800+
[bar]
801+
web_identity_token_file = ${webIdentityTokenFile}
802+
role_arn = ${roleArn}
803+
role_session_name = ${roleSessionName}
804+
805+
[foo]
806+
role_arn = ${fooRoleArn}
807+
role_session_name = ${fooSessionName}
808+
source_profile = bar`.trim()
809+
);
810+
811+
const provider = fromIni({
812+
profile: "foo",
813+
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
814+
expect(sourceCreds).toEqual(DEFAULT_CREDS);
815+
expect(params.RoleArn).toEqual(fooRoleArn);
816+
expect(params.RoleSessionName).toEqual(fooSessionName);
817+
return Promise.resolve(FOO_CREDS);
818+
},
819+
roleAssumerWithWebIdentity,
820+
});
821+
822+
expect(await provider()).toEqual(FOO_CREDS);
823+
expect(fromTokenFile).toHaveBeenCalledTimes(1);
824+
expect(fromTokenFile).toHaveBeenCalledWith({
825+
webIdentityTokenFile,
826+
roleArn,
827+
roleSessionName,
828+
roleAssumerWithWebIdentity,
829+
});
830+
});
831+
832+
it("should call fromTokenFile without roleSessionName if not present in profile", async () => {
833+
(fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
834+
const webIdentityTokenFile = "/temp/foo/token";
835+
const roleArn = "arn:aws:iam::123456789:role/bar";
836+
const roleAssumerWithWebIdentity = jest.fn();
837+
__addMatcher(
838+
join(homedir(), ".aws", "credentials"),
839+
`
840+
[foo]
841+
web_identity_token_file = ${webIdentityTokenFile}
842+
role_arn = ${roleArn}`.trim()
843+
);
844+
845+
const provider = fromIni({
846+
profile: "foo",
847+
roleAssumerWithWebIdentity,
848+
});
849+
850+
expect(await provider()).toEqual(FOO_CREDS);
851+
expect(fromTokenFile).toHaveBeenCalledTimes(1);
852+
expect(fromTokenFile).toHaveBeenCalledWith({
853+
webIdentityTokenFile,
854+
roleArn,
855+
roleAssumerWithWebIdentity,
856+
});
857+
});
858+
});
859+
752860
it("should prefer credentials in ~/.aws/credentials to those in ~/.aws/config", async () => {
753861
__addMatcher(
754862
join(homedir(), ".aws", "credentials"),

packages/credential-provider-ini/src/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
12
import { ProviderError } from "@aws-sdk/property-provider";
23
import {
34
loadSharedConfigFiles,
@@ -78,6 +79,15 @@ export interface FromIniInit extends SourceProfileInit {
7879
* @param params
7980
*/
8081
roleAssumer?: (sourceCreds: Credentials, params: AssumeRoleParams) => Promise<Credentials>;
82+
83+
/**
84+
* A function that assumes a role with web identity and returns a promise fulfilled with
85+
* credentials for the assumed role.
86+
*
87+
* @param sourceCreds The credentials with which to assume a role.
88+
* @param params
89+
*/
90+
roleAssumerWithWebIdentity?: (params: AssumeRoleWithWebIdentityParams) => Promise<Credentials>;
8191
}
8292

8393
interface StaticCredsProfile extends Profile {
@@ -93,12 +103,24 @@ const isStaticCredsProfile = (arg: any): arg is StaticCredsProfile =>
93103
typeof arg.aws_secret_access_key === "string" &&
94104
["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1;
95105

106+
interface WebIdentityProfile extends Profile {
107+
web_identity_token_file: string;
108+
role_arn: string;
109+
role_session_name?: string;
110+
}
111+
112+
const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile =>
113+
Boolean(arg) &&
114+
typeof arg === "object" &&
115+
typeof arg.web_identity_token_file === "string" &&
116+
typeof arg.role_arn === "string" &&
117+
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1;
96118
interface AssumeRoleProfile extends Profile {
97119
role_arn: string;
98120
source_profile: string;
99121
}
100122

101-
const isAssumeRoleProfile = (arg: any): arg is AssumeRoleProfile =>
123+
const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleProfile =>
102124
Boolean(arg) &&
103125
typeof arg === "object" &&
104126
typeof arg.role_arn === "string" &&
@@ -155,7 +177,7 @@ const resolveProfileData = async (
155177

156178
// If this is the first profile visited, role assumption keys should be
157179
// given precedence over static credentials.
158-
if (isAssumeRoleProfile(data)) {
180+
if (isAssumeRoleWithSourceProfile(data)) {
159181
const {
160182
external_id: ExternalId,
161183
mfa_serial,
@@ -205,6 +227,12 @@ const resolveProfileData = async (
205227
return resolveStaticCredentials(data);
206228
}
207229

230+
// If no static credentials are present, attempt to assume role with
231+
// web identity if web_identity_token_file and role_arn is available
232+
if (isWebIdentityProfile(data)) {
233+
return resolveWebIdentityCredentials(data, options);
234+
}
235+
208236
// If the profile cannot be parsed or contains neither static credentials
209237
// nor role assumption metadata, throw an error. This should be considered a
210238
// terminal resolution error if a profile has been specified by the user
@@ -219,3 +247,11 @@ const resolveStaticCredentials = (profile: StaticCredsProfile): Promise<Credenti
219247
secretAccessKey: profile.aws_secret_access_key,
220248
sessionToken: profile.aws_session_token,
221249
});
250+
251+
const resolveWebIdentityCredentials = async (profile: WebIdentityProfile, options: FromIniInit): Promise<Credentials> =>
252+
fromTokenFile({
253+
webIdentityTokenFile: profile.web_identity_token_file,
254+
roleArn: profile.role_arn,
255+
roleSessionName: profile.role_session_name,
256+
roleAssumerWithWebIdentity: options.roleAssumerWithWebIdentity,
257+
})();

0 commit comments

Comments
 (0)