From b90e15f35e9be16608d456af5d41718c5e376ab7 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Tue, 13 Apr 2021 01:47:34 +0000 Subject: [PATCH 1/3] feat(credential-provider-ini): support credential_source in shared file --- packages/credential-provider-ini/package.json | 2 + .../credential-provider-ini/src/index.spec.ts | 131 ++++++++++++++++++ packages/credential-provider-ini/src/index.ts | 62 +++++++-- 3 files changed, 187 insertions(+), 8 deletions(-) diff --git a/packages/credential-provider-ini/package.json b/packages/credential-provider-ini/package.json index 7b9f4b1848db..c5639322294a 100644 --- a/packages/credential-provider-ini/package.json +++ b/packages/credential-provider-ini/package.json @@ -21,6 +21,8 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-sdk/credential-provider-env": "3.12.0", + "@aws-sdk/credential-provider-imds": "3.12.0", "@aws-sdk/credential-provider-web-identity": "3.12.0", "@aws-sdk/property-provider": "3.12.0", "@aws-sdk/shared-ini-file-loader": "3.12.0", diff --git a/packages/credential-provider-ini/src/index.spec.ts b/packages/credential-provider-ini/src/index.spec.ts index b45889afea98..d723b7b52523 100644 --- a/packages/credential-provider-ini/src/index.spec.ts +++ b/packages/credential-provider-ini/src/index.spec.ts @@ -1,3 +1,5 @@ +import { fromEnv } from "@aws-sdk/credential-provider-env"; +import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity"; import { ENV_CONFIG_PATH, ENV_CREDENTIALS_PATH } from "@aws-sdk/shared-ini-file-loader"; import { Credentials } from "@aws-sdk/types"; @@ -54,6 +56,10 @@ import { homedir } from "os"; jest.mock("@aws-sdk/credential-provider-web-identity"); +jest.mock("@aws-sdk/credential-provider-imds"); + +jest.mock("@aws-sdk/credential-provider-env"); + const DEFAULT_CREDS = { accessKeyId: "AKIAIOSFODNN7EXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", @@ -752,6 +758,131 @@ source_profile = default`.trim() tryNextLink: false, }); }); + + describe("assume role with source credential providers", () => { + const setUpTest = (credentialSource: string) => { + const roleArn = `arn:aws:iam::123456789:role/${credentialSource}`; + const roleSessionName = `${credentialSource}SessionName`; + const mfaSerial = `mfaSerial${credentialSource}`; + const mfaCode = Date.now().toString(10); + __addMatcher( + join(homedir(), ".aws", "credentials"), + ` +[default] +role_arn = ${roleArn} +role_session_name = ${roleSessionName} +mfa_serial = ${mfaSerial} +credential_source = ${credentialSource} + `.trim() + ); + return { + roleArn, + roleSessionName, + mfaSerial, + mfaCode, + }; + }; + + it("should assume role from source credentials from EC2 instance provider", async () => { + (fromInstanceMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS)); + const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Ec2InstanceMetadata"); + const provider = fromIni({ + mfaCodeProvider(mfa) { + expect(mfa).toBe(mfaSerial); + return Promise.resolve(mfaCode); + }, + roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { + expect(fromInstanceMetadata as jest.Mock).toBeCalledTimes(1); + expect(params.RoleSessionName).toBe(roleSessionName); + expect(params.RoleArn).toBe(roleArn); + expect(params.TokenCode).toBe(mfaCode); + expect(sourceCreds).toEqual(FOO_CREDS); + return Promise.resolve(FIZZ_CREDS); + }, + }); + expect(await provider()).toEqual(FIZZ_CREDS); + }); + + it("should assume role from source credentials from environmental variable provider", async () => { + (fromEnv as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS)); + const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("Environment"); + const provider = fromIni({ + mfaCodeProvider(mfa) { + expect(mfa).toBe(mfaSerial); + return Promise.resolve(mfaCode); + }, + roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { + expect(fromEnv as jest.Mock).toBeCalledTimes(1); + expect(params.RoleSessionName).toBe(roleSessionName); + expect(params.RoleArn).toBe(roleArn); + expect(params.TokenCode).toBe(mfaCode); + expect(sourceCreds).toEqual(FOO_CREDS); + return Promise.resolve(FIZZ_CREDS); + }, + }); + expect(await provider()).toEqual(FIZZ_CREDS); + }); + + it("should assume role from source credentials from ECS container provider", async () => { + (fromContainerMetadata as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS)); + const { roleArn, roleSessionName, mfaCode, mfaSerial } = setUpTest("EcsContainer"); + const provider = fromIni({ + mfaCodeProvider(mfa) { + expect(mfa).toBe(mfaSerial); + return Promise.resolve(mfaCode); + }, + roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { + expect(fromContainerMetadata as jest.Mock).toBeCalledTimes(1); + expect(params.RoleSessionName).toBe(roleSessionName); + expect(params.RoleArn).toBe(roleArn); + expect(params.TokenCode).toBe(mfaCode); + expect(sourceCreds).toEqual(FOO_CREDS); + return Promise.resolve(FIZZ_CREDS); + }, + }); + expect(await provider()).toEqual(FIZZ_CREDS); + }); + + it("should throw if source credentials provider is not supported", () => { + const someProvider = "SomeProvider"; + setUpTest(someProvider); + const provider = fromIni({ + roleAssumer(): Promise { + return Promise.resolve(FIZZ_CREDS); + }, + }); + return expect(async () => await provider()).rejects.toMatchObject({ + message: + `Unsupported credential source in profile default. Got ${someProvider}, expect EcsContainer or ` + + `Ec2InstanceMetadata or Environment`, + }); + }); + + it("should throw if both source profile and credential source is specified", async () => { + __addMatcher( + join(homedir(), ".aws", "credentials"), + ` +[profile A] +aws_access_key_id = abc123 +aws_secret_access_key = def456 +[default] +role_arn = arn:aws:iam::123456789:role/Role +credential_source = Ec2InstanceMetadata +source_profile = A + `.trim() + ); + try { + await fromIni({ + roleAssumer(): Promise { + return Promise.resolve(FIZZ_CREDS); + }, + })(); + fail("Expected error to be thrown"); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); }); describe("assume role with web identity", () => { diff --git a/packages/credential-provider-ini/src/index.ts b/packages/credential-provider-ini/src/index.ts index 0182fa309582..7807441728d9 100755 --- a/packages/credential-provider-ini/src/index.ts +++ b/packages/credential-provider-ini/src/index.ts @@ -1,3 +1,5 @@ +import { fromEnv } from "@aws-sdk/credential-provider-env"; +import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity"; import { ProviderError } from "@aws-sdk/property-provider"; import { @@ -115,16 +117,33 @@ const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile => typeof arg.web_identity_token_file === "string" && typeof arg.role_arn === "string" && ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1; -interface AssumeRoleProfile extends Profile { + +interface AssumeRoleWithSourceProfile extends Profile { role_arn: string; source_profile: string; } -const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleProfile => +interface AssumeRoleWithProviderProfile extends Profile { + role_arn: string; + credential_source: string; +} + +const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile => Boolean(arg) && typeof arg === "object" && typeof arg.role_arn === "string" && typeof arg.source_profile === "string" && + typeof arg.credential_source === "undefined" && + ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 && + ["undefined", "string"].indexOf(typeof arg.external_id) > -1 && + ["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1; + +const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile => + Boolean(arg) && + typeof arg === "object" && + typeof arg.role_arn === "string" && + typeof arg.credential_source === "string" && + typeof arg.source_profile === "undefined" && ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 && ["undefined", "string"].indexOf(typeof arg.external_id) > -1 && ["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1; @@ -177,13 +196,14 @@ const resolveProfileData = async ( // If this is the first profile visited, role assumption keys should be // given precedence over static credentials. - if (isAssumeRoleWithSourceProfile(data)) { + if (isAssumeRoleWithSourceProfile(data) || isAssumeRoleWithProviderProfile(data)) { const { external_id: ExternalId, mfa_serial, role_arn: RoleArn, role_session_name: RoleSessionName = "aws-sdk-js-" + Date.now(), source_profile, + credential_source, } = data; if (!options.roleAssumer) { @@ -193,7 +213,7 @@ const resolveProfileData = async ( ); } - if (source_profile in visitedProfiles) { + if (source_profile && source_profile in visitedProfiles) { throw new ProviderError( `Detected a cycle attempting to resolve credentials for profile` + ` ${getMasterProfileName(options)}. Profiles visited: ` + @@ -202,10 +222,13 @@ const resolveProfileData = async ( ); } - const sourceCreds = resolveProfileData(source_profile, profiles, options, { - ...visitedProfiles, - [source_profile]: true, - }); + const sourceCreds = source_profile + ? resolveProfileData(source_profile, profiles, options, { + ...visitedProfiles, + [source_profile]: true, + }) + : resolveCredentialSource(credential_source!, profileName)(); + const params: AssumeRoleParams = { RoleArn, RoleSessionName, ExternalId }; if (mfa_serial) { if (!options.mfaCodeProvider) { @@ -241,6 +264,29 @@ const resolveProfileData = async ( throw new ProviderError(`Profile ${profileName} could not be found or parsed in shared` + ` credentials file.`); }; +/** + * Resolve the `credential_source` entry from the profile, and return the + * credential providers respectively. No memoization is needed for the + * credential source providers because memoization should be added outside the + * fromIni() provider. The source credential needs to be refreshed every time + * fromIni() is called. + */ +const resolveCredentialSource = (credentialSource: string, profileName: string): CredentialProvider => { + const sourceProvidersMap: { [name: string]: () => CredentialProvider } = { + EcsContainer: fromContainerMetadata, + Ec2InstanceMetadata: fromInstanceMetadata, + Environment: fromEnv, + }; + if (credentialSource in sourceProvidersMap) { + return sourceProvidersMap[credentialSource](); + } else { + throw new ProviderError( + `Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` + + `expect EcsContainer or Ec2InstanceMetadata or Environment` + ); + } +}; + const resolveStaticCredentials = (profile: StaticCredsProfile): Promise => Promise.resolve({ accessKeyId: profile.aws_access_key_id, From 02b8f5798ad962d8427ba9a0aa4817cb6e5fc617 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Wed, 14 Apr 2021 00:13:23 +0000 Subject: [PATCH 2/3] docs(credential-provider-ini): update readme --- packages/credential-provider-ini/README.md | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/credential-provider-ini/README.md b/packages/credential-provider-ini/README.md index adb383bc503f..eeb23621ced7 100644 --- a/packages/credential-provider-ini/README.md +++ b/packages/credential-provider-ini/README.md @@ -82,7 +82,7 @@ aws_access_key_id=foo4 aws_secret_access_key=bar4 ``` -### source profile with static credentials +### profile with source profile ```ini [second] @@ -94,6 +94,30 @@ source_profile=first role_arn=arn:aws:iam::123456789012:role/example-role-arn ``` +### profile with source provider + +You can supply `credential_source` options to tell the SDK where to source +credentials for the call to `AssumeRole`. The supported credential providers are +listed bellow: + +```ini +[default] +role_arn=arn:aws:iam::123456789012:role/example-role-arn +credential_source = Ec2InstanceMetadata +``` + +```ini +[default] +role_arn=arn:aws:iam::123456789012:role/example-role-arn +credential_source = Environment +``` + +```ini +[default] +role_arn=arn:aws:iam::123456789012:role/example-role-arn +credential_source = EcsContainer +``` + ### profile with web_identity_token_file ```ini From d408cb028b0b03fbdc4add8d9751d5971a7c5444 Mon Sep 17 00:00:00 2001 From: AllanZhengYP Date: Thu, 15 Apr 2021 09:22:51 -0700 Subject: [PATCH 3/3] fix: address feedbacks --- .../credential-provider-ini/src/index.spec.ts | 4 ++-- packages/credential-provider-ini/src/index.ts | 18 ++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/credential-provider-ini/src/index.spec.ts b/packages/credential-provider-ini/src/index.spec.ts index d723b7b52523..a0f4eb4365a7 100644 --- a/packages/credential-provider-ini/src/index.spec.ts +++ b/packages/credential-provider-ini/src/index.spec.ts @@ -853,8 +853,8 @@ credential_source = ${credentialSource} }); return expect(async () => await provider()).rejects.toMatchObject({ message: - `Unsupported credential source in profile default. Got ${someProvider}, expect EcsContainer or ` + - `Ec2InstanceMetadata or Environment`, + `Unsupported credential source in profile default. Got ${someProvider}, expected EcsContainer or ` + + `Ec2InstanceMetadata or Environment.`, }); }); diff --git a/packages/credential-provider-ini/src/index.ts b/packages/credential-provider-ini/src/index.ts index 7807441728d9..5824b52fcfa3 100755 --- a/packages/credential-provider-ini/src/index.ts +++ b/packages/credential-provider-ini/src/index.ts @@ -128,25 +128,19 @@ interface AssumeRoleWithProviderProfile extends Profile { credential_source: string; } -const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile => +const isAssumeRoleProfile = (arg: any) => Boolean(arg) && typeof arg === "object" && typeof arg.role_arn === "string" && - typeof arg.source_profile === "string" && - typeof arg.credential_source === "undefined" && ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 && ["undefined", "string"].indexOf(typeof arg.external_id) > -1 && ["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1; +const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile => + isAssumeRoleProfile(arg) && typeof arg.source_profile === "string" && typeof arg.credential_source === "undefined"; + const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile => - Boolean(arg) && - typeof arg === "object" && - typeof arg.role_arn === "string" && - typeof arg.credential_source === "string" && - typeof arg.source_profile === "undefined" && - ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1 && - ["undefined", "string"].indexOf(typeof arg.external_id) > -1 && - ["undefined", "string"].indexOf(typeof arg.mfa_serial) > -1; + isAssumeRoleProfile(arg) && typeof arg.credential_source === "string" && typeof arg.source_profile === "undefined"; /** * Creates a credential provider that will read from ini files and supports @@ -282,7 +276,7 @@ const resolveCredentialSource = (credentialSource: string, profileName: string): } else { throw new ProviderError( `Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` + - `expect EcsContainer or Ec2InstanceMetadata or Environment` + `expected EcsContainer or Ec2InstanceMetadata or Environment.` ); } };