Skip to content

Commit 135a12c

Browse files
authored
feat(credential-provider-ini): support credential_source in shared file (#2237)
* feat(credential-provider-ini): support credential_source in shared file * docs(credential-provider-ini): update readme * fix: address feedbacks
1 parent f0ef766 commit 135a12c

File tree

4 files changed

+207
-10
lines changed

4 files changed

+207
-10
lines changed

packages/credential-provider-ini/README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ aws_access_key_id=foo4
8282
aws_secret_access_key=bar4
8383
```
8484

85-
### source profile with static credentials
85+
### profile with source profile
8686

8787
```ini
8888
[second]
@@ -94,6 +94,30 @@ source_profile=first
9494
role_arn=arn:aws:iam::123456789012:role/example-role-arn
9595
```
9696

97+
### profile with source provider
98+
99+
You can supply `credential_source` options to tell the SDK where to source
100+
credentials for the call to `AssumeRole`. The supported credential providers are
101+
listed bellow:
102+
103+
```ini
104+
[default]
105+
role_arn=arn:aws:iam::123456789012:role/example-role-arn
106+
credential_source = Ec2InstanceMetadata
107+
```
108+
109+
```ini
110+
[default]
111+
role_arn=arn:aws:iam::123456789012:role/example-role-arn
112+
credential_source = Environment
113+
```
114+
115+
```ini
116+
[default]
117+
role_arn=arn:aws:iam::123456789012:role/example-role-arn
118+
credential_source = EcsContainer
119+
```
120+
97121
### profile with web_identity_token_file
98122

99123
```ini

packages/credential-provider-ini/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
},
2222
"license": "Apache-2.0",
2323
"dependencies": {
24+
"@aws-sdk/credential-provider-env": "3.12.0",
25+
"@aws-sdk/credential-provider-imds": "3.12.0",
2426
"@aws-sdk/credential-provider-web-identity": "3.12.0",
2527
"@aws-sdk/property-provider": "3.12.0",
2628
"@aws-sdk/shared-ini-file-loader": "3.12.0",

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

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fromEnv } from "@aws-sdk/credential-provider-env";
2+
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
13
import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
24
import { ENV_CONFIG_PATH, ENV_CREDENTIALS_PATH } from "@aws-sdk/shared-ini-file-loader";
35
import { Credentials } from "@aws-sdk/types";
@@ -54,6 +56,10 @@ import { homedir } from "os";
5456

5557
jest.mock("@aws-sdk/credential-provider-web-identity");
5658

59+
jest.mock("@aws-sdk/credential-provider-imds");
60+
61+
jest.mock("@aws-sdk/credential-provider-env");
62+
5763
const DEFAULT_CREDS = {
5864
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
5965
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
@@ -752,6 +758,131 @@ source_profile = default`.trim()
752758
tryNextLink: false,
753759
});
754760
});
761+
762+
describe("assume role with source credential providers", () => {
763+
const setUpTest = (credentialSource: string) => {
764+
const roleArn = `arn:aws:iam::123456789:role/${credentialSource}`;
765+
const roleSessionName = `${credentialSource}SessionName`;
766+
const mfaSerial = `mfaSerial${credentialSource}`;
767+
const mfaCode = Date.now().toString(10);
768+
__addMatcher(
769+
join(homedir(), ".aws", "credentials"),
770+
`
771+
[default]
772+
role_arn = ${roleArn}
773+
role_session_name = ${roleSessionName}
774+
mfa_serial = ${mfaSerial}
775+
credential_source = ${credentialSource}
776+
`.trim()
777+
);
778+
return {
779+
roleArn,
780+
roleSessionName,
781+
mfaSerial,
782+
mfaCode,
783+
};
784+
};
785+
786+
it("should assume role from source credentials from EC2 instance provider", async () => {
787+
(fromInstanceMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
788+
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Ec2InstanceMetadata");
789+
const provider = fromIni({
790+
mfaCodeProvider(mfa) {
791+
expect(mfa).toBe(mfaSerial);
792+
return Promise.resolve(mfaCode);
793+
},
794+
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
795+
expect(fromInstanceMetadata as jest.Mock).toBeCalledTimes(1);
796+
expect(params.RoleSessionName).toBe(roleSessionName);
797+
expect(params.RoleArn).toBe(roleArn);
798+
expect(params.TokenCode).toBe(mfaCode);
799+
expect(sourceCreds).toEqual(FOO_CREDS);
800+
return Promise.resolve(FIZZ_CREDS);
801+
},
802+
});
803+
expect(await provider()).toEqual(FIZZ_CREDS);
804+
});
805+
806+
it("should assume role from source credentials from environmental variable provider", async () => {
807+
(fromEnv as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
808+
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Environment");
809+
const provider = fromIni({
810+
mfaCodeProvider(mfa) {
811+
expect(mfa).toBe(mfaSerial);
812+
return Promise.resolve(mfaCode);
813+
},
814+
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
815+
expect(fromEnv as jest.Mock).toBeCalledTimes(1);
816+
expect(params.RoleSessionName).toBe(roleSessionName);
817+
expect(params.RoleArn).toBe(roleArn);
818+
expect(params.TokenCode).toBe(mfaCode);
819+
expect(sourceCreds).toEqual(FOO_CREDS);
820+
return Promise.resolve(FIZZ_CREDS);
821+
},
822+
});
823+
expect(await provider()).toEqual(FIZZ_CREDS);
824+
});
825+
826+
it("should assume role from source credentials from ECS container provider", async () => {
827+
(fromContainerMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS));
828+
const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("EcsContainer");
829+
const provider = fromIni({
830+
mfaCodeProvider(mfa) {
831+
expect(mfa).toBe(mfaSerial);
832+
return Promise.resolve(mfaCode);
833+
},
834+
roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise<Credentials> {
835+
expect(fromContainerMetadata as jest.Mock).toBeCalledTimes(1);
836+
expect(params.RoleSessionName).toBe(roleSessionName);
837+
expect(params.RoleArn).toBe(roleArn);
838+
expect(params.TokenCode).toBe(mfaCode);
839+
expect(sourceCreds).toEqual(FOO_CREDS);
840+
return Promise.resolve(FIZZ_CREDS);
841+
},
842+
});
843+
expect(await provider()).toEqual(FIZZ_CREDS);
844+
});
845+
846+
it("should throw if source credentials provider is not supported", () => {
847+
const someProvider = "SomeProvider";
848+
setUpTest(someProvider);
849+
const provider = fromIni({
850+
roleAssumer(): Promise<Credentials> {
851+
return Promise.resolve(FIZZ_CREDS);
852+
},
853+
});
854+
return expect(async () => await provider()).rejects.toMatchObject({
855+
message:
856+
`Unsupported credential source in profile default. Got ${someProvider}, expected EcsContainer or ` +
857+
`Ec2InstanceMetadata or Environment.`,
858+
});
859+
});
860+
861+
it("should throw if both source profile and credential source is specified", async () => {
862+
__addMatcher(
863+
join(homedir(), ".aws", "credentials"),
864+
`
865+
[profile A]
866+
aws_access_key_id = abc123
867+
aws_secret_access_key = def456
868+
[default]
869+
role_arn = arn:aws:iam::123456789:role/Role
870+
credential_source = Ec2InstanceMetadata
871+
source_profile = A
872+
`.trim()
873+
);
874+
try {
875+
await fromIni({
876+
roleAssumer(): Promise<Credentials> {
877+
return Promise.resolve(FIZZ_CREDS);
878+
},
879+
})();
880+
fail("Expected error to be thrown");
881+
} catch (e) {
882+
expect(e).toBeDefined();
883+
}
884+
});
885+
});
755886
});
756887

757888
describe("assume role with web identity", () => {

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fromEnv } from "@aws-sdk/credential-provider-env";
2+
import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds";
13
import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
24
import { ProviderError } from "@aws-sdk/property-provider";
35
import {
@@ -115,20 +117,31 @@ const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile =>
115117
typeof arg.web_identity_token_file === "string" &&
116118
typeof arg.role_arn === "string" &&
117119
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1;
118-
interface AssumeRoleProfile extends Profile {
120+
121+
interface AssumeRoleWithSourceProfile extends Profile {
119122
role_arn: string;
120123
source_profile: string;
121124
}
122125

123-
const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleProfile =>
126+
interface AssumeRoleWithProviderProfile extends Profile {
127+
role_arn: string;
128+
credential_source: string;
129+
}
130+
131+
const isAssumeRoleProfile = (arg: any) =>
124132
Boolean(arg) &&
125133
typeof arg === "object" &&
126134
typeof arg.role_arn === "string" &&
127-
typeof arg.source_profile === "string" &&
128135
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 &&
129136
["undefined", "string"].indexOf(typeof arg.external_id) > -1 &&
130137
["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1;
131138

139+
const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile =>
140+
isAssumeRoleProfile(arg) && typeof arg.source_profile === "string" && typeof arg.credential_source === "undefined";
141+
142+
const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile =>
143+
isAssumeRoleProfile(arg) && typeof arg.credential_source === "string" && typeof arg.source_profile === "undefined";
144+
132145
/**
133146
* Creates a credential provider that will read from ini files and supports
134147
* role assumption and multi-factor authentication.
@@ -177,13 +190,14 @@ const resolveProfileData = async (
177190

178191
// If this is the first profile visited, role assumption keys should be
179192
// given precedence over static credentials.
180-
if (isAssumeRoleWithSourceProfile(data)) {
193+
if (isAssumeRoleWithSourceProfile(data) || isAssumeRoleWithProviderProfile(data)) {
181194
const {
182195
external_id: ExternalId,
183196
mfa_serial,
184197
role_arn: RoleArn,
185198
role_session_name: RoleSessionName = "aws-sdk-js-" + Date.now(),
186199
source_profile,
200+
credential_source,
187201
} = data;
188202

189203
if (!options.roleAssumer) {
@@ -193,7 +207,7 @@ const resolveProfileData = async (
193207
);
194208
}
195209

196-
if (source_profile in visitedProfiles) {
210+
if (source_profile && source_profile in visitedProfiles) {
197211
throw new ProviderError(
198212
`Detected a cycle attempting to resolve credentials for profile` +
199213
` ${getMasterProfileName(options)}. Profiles visited: ` +
@@ -202,10 +216,13 @@ const resolveProfileData = async (
202216
);
203217
}
204218

205-
const sourceCreds = resolveProfileData(source_profile, profiles, options, {
206-
...visitedProfiles,
207-
[source_profile]: true,
208-
});
219+
const sourceCreds = source_profile
220+
? resolveProfileData(source_profile, profiles, options, {
221+
...visitedProfiles,
222+
[source_profile]: true,
223+
})
224+
: resolveCredentialSource(credential_source!, profileName)();
225+
209226
const params: AssumeRoleParams = { RoleArn, RoleSessionName, ExternalId };
210227
if (mfa_serial) {
211228
if (!options.mfaCodeProvider) {
@@ -241,6 +258,29 @@ const resolveProfileData = async (
241258
throw new ProviderError(`Profile ${profileName} could not be found or parsed in shared` + ` credentials file.`);
242259
};
243260

261+
/**
262+
* Resolve the `credential_source` entry from the profile, and return the
263+
* credential providers respectively. No memoization is needed for the
264+
* credential source providers because memoization should be added outside the
265+
* fromIni() provider. The source credential needs to be refreshed every time
266+
* fromIni() is called.
267+
*/
268+
const resolveCredentialSource = (credentialSource: string, profileName: string): CredentialProvider => {
269+
const sourceProvidersMap: { [name: string]: () => CredentialProvider } = {
270+
EcsContainer: fromContainerMetadata,
271+
Ec2InstanceMetadata: fromInstanceMetadata,
272+
Environment: fromEnv,
273+
};
274+
if (credentialSource in sourceProvidersMap) {
275+
return sourceProvidersMap[credentialSource]();
276+
} else {
277+
throw new ProviderError(
278+
`Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` +
279+
`expected EcsContainer or Ec2InstanceMetadata or Environment.`
280+
);
281+
}
282+
};
283+
244284
const resolveStaticCredentials = (profile: StaticCredsProfile): Promise<Credentials> =>
245285
Promise.resolve({
246286
accessKeyId: profile.aws_access_key_id,

0 commit comments

Comments
 (0)