Skip to content

Commit b90e15f

Browse files
committed
feat(credential-provider-ini): support credential_source in shared file
1 parent d6a67d7 commit b90e15f

File tree

3 files changed

+187
-8
lines changed

3 files changed

+187
-8
lines changed

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}, expect 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: 54 additions & 8 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,16 +117,33 @@ 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 isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile =>
124132
Boolean(arg) &&
125133
typeof arg === "object" &&
126134
typeof arg.role_arn === "string" &&
127135
typeof arg.source_profile === "string" &&
136+
typeof arg.credential_source === "undefined" &&
137+
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 &&
138+
["undefined", "string"].indexOf(typeof arg.external_id) > -1 &&
139+
["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1;
140+
141+
const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile =>
142+
Boolean(arg) &&
143+
typeof arg === "object" &&
144+
typeof arg.role_arn === "string" &&
145+
typeof arg.credential_source === "string" &&
146+
typeof arg.source_profile === "undefined" &&
128147
["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 &&
129148
["undefined", "string"].indexOf(typeof arg.external_id) > -1 &&
130149
["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1;
@@ -177,13 +196,14 @@ const resolveProfileData = async (
177196

178197
// If this is the first profile visited, role assumption keys should be
179198
// given precedence over static credentials.
180-
if (isAssumeRoleWithSourceProfile(data)) {
199+
if (isAssumeRoleWithSourceProfile(data) || isAssumeRoleWithProviderProfile(data)) {
181200
const {
182201
external_id: ExternalId,
183202
mfa_serial,
184203
role_arn: RoleArn,
185204
role_session_name: RoleSessionName = "aws-sdk-js-" + Date.now(),
186205
source_profile,
206+
credential_source,
187207
} = data;
188208

189209
if (!options.roleAssumer) {
@@ -193,7 +213,7 @@ const resolveProfileData = async (
193213
);
194214
}
195215

196-
if (source_profile in visitedProfiles) {
216+
if (source_profile && source_profile in visitedProfiles) {
197217
throw new ProviderError(
198218
`Detected a cycle attempting to resolve credentials for profile` +
199219
` ${getMasterProfileName(options)}. Profiles visited: ` +
@@ -202,10 +222,13 @@ const resolveProfileData = async (
202222
);
203223
}
204224

205-
const sourceCreds = resolveProfileData(source_profile, profiles, options, {
206-
...visitedProfiles,
207-
[source_profile]: true,
208-
});
225+
const sourceCreds = source_profile
226+
? resolveProfileData(source_profile, profiles, options, {
227+
...visitedProfiles,
228+
[source_profile]: true,
229+
})
230+
: resolveCredentialSource(credential_source!, profileName)();
231+
209232
const params: AssumeRoleParams = { RoleArn, RoleSessionName, ExternalId };
210233
if (mfa_serial) {
211234
if (!options.mfaCodeProvider) {
@@ -241,6 +264,29 @@ const resolveProfileData = async (
241264
throw new ProviderError(`Profile ${profileName} could not be found or parsed in shared` + ` credentials file.`);
242265
};
243266

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

0 commit comments

Comments
 (0)