From 7c891b215cf3a9ea343447ced51e5d7be86caba9 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Wed, 9 Feb 2022 13:26:10 -0800 Subject: [PATCH] feat(credential-provider-ini): refactor into modular components (#3289) --- .../src/fromIni.spec.ts | 67 + .../credential-provider-ini/src/fromIni.ts | 47 + .../credential-provider-ini/src/index.spec.ts | 1329 ----------------- packages/credential-provider-ini/src/index.ts | 267 +--- .../src/resolveAssumeRoleCredentials.spec.ts | 258 ++++ .../src/resolveAssumeRoleCredentials.ts | 119 ++ .../src/resolveCredentialSource.spec.ts | 65 + .../src/resolveCredentialSource.ts | 27 + .../src/resolveProfileData.spec.ts | 122 ++ .../src/resolveProfileData.ts | 54 + .../src/resolveSsoCredentials.spec.ts | 98 ++ .../src/resolveSsoCredentials.ts | 14 + .../src/resolveStaticCredentials.spec.ts | 49 + .../src/resolveStaticCredentials.ts | 22 + .../src/resolveWebIdentityCredentials.spec.ts | 94 ++ .../src/resolveWebIdentityCredentials.ts | 29 + 16 files changed, 1066 insertions(+), 1595 deletions(-) create mode 100644 packages/credential-provider-ini/src/fromIni.spec.ts create mode 100644 packages/credential-provider-ini/src/fromIni.ts delete mode 100644 packages/credential-provider-ini/src/index.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveAssumeRoleCredentials.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts create mode 100644 packages/credential-provider-ini/src/resolveCredentialSource.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveCredentialSource.ts create mode 100644 packages/credential-provider-ini/src/resolveProfileData.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveProfileData.ts create mode 100644 packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveSsoCredentials.ts create mode 100644 packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveStaticCredentials.ts create mode 100644 packages/credential-provider-ini/src/resolveWebIdentityCredentials.spec.ts create mode 100644 packages/credential-provider-ini/src/resolveWebIdentityCredentials.ts diff --git a/packages/credential-provider-ini/src/fromIni.spec.ts b/packages/credential-provider-ini/src/fromIni.spec.ts new file mode 100644 index 000000000000..898873beeebe --- /dev/null +++ b/packages/credential-provider-ini/src/fromIni.spec.ts @@ -0,0 +1,67 @@ +import { AssumeRoleWithWebIdentityParams } from "@aws-sdk/credential-provider-web-identity"; +import { CredentialProvider, Credentials } from "@aws-sdk/types"; +import { getMasterProfileName, parseKnownFiles } from "@aws-sdk/util-credentials"; + +import { fromIni } from "./fromIni"; +import { AssumeRoleParams } from "./resolveAssumeRoleCredentials"; +import { resolveProfileData } from "./resolveProfileData"; + +jest.mock("@aws-sdk/util-credentials"); +jest.mock("./resolveProfileData"); + +describe(fromIni.name, () => { + const mockMasterProfileName = "mockMasterProfileName"; + const mockProfileName = "mockProfileName"; + const mockInit = { profile: mockProfileName }; + const mockProfiles = { [mockProfileName]: { key: "value" } }; + + beforeEach(() => { + (parseKnownFiles as jest.Mock).mockResolvedValue(mockProfiles); + (getMasterProfileName as jest.Mock).mockReturnValue(mockMasterProfileName); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("rethrows error if parsing known files fails", async () => { + const expectedError = new Error("from parseKnownFiles"); + (parseKnownFiles as jest.Mock).mockRejectedValue(expectedError); + try { + await fromIni(mockInit)(); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(parseKnownFiles).toHaveBeenCalledWith(mockInit); + expect(getMasterProfileName).not.toHaveBeenCalled(); + expect(resolveProfileData).not.toHaveBeenCalled(); + }); + + it("rethrows error if resolving process creds fails", async () => { + const expectedError = new Error("from resolveProcessCredentials"); + (resolveProfileData as jest.Mock).mockRejectedValue(expectedError); + try { + await fromIni(mockInit)(); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(parseKnownFiles).toHaveBeenCalledWith(mockInit); + expect(getMasterProfileName).toHaveBeenCalledWith(mockInit); + expect(resolveProfileData).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles, mockInit); + }); + + it("returns resolved process creds", async () => { + const expectedCreds: Credentials = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + (resolveProfileData as jest.Mock).mockResolvedValue(expectedCreds); + const receivedCreds = await fromIni(mockInit)(); + expect(receivedCreds).toStrictEqual(expectedCreds); + expect(parseKnownFiles).toHaveBeenCalledWith(mockInit); + expect(getMasterProfileName).toHaveBeenCalledWith(mockInit); + expect(resolveProfileData).toHaveBeenCalledWith(mockMasterProfileName, mockProfiles, mockInit); + }); +}); diff --git a/packages/credential-provider-ini/src/fromIni.ts b/packages/credential-provider-ini/src/fromIni.ts new file mode 100644 index 000000000000..d154de709ecd --- /dev/null +++ b/packages/credential-provider-ini/src/fromIni.ts @@ -0,0 +1,47 @@ +import { AssumeRoleWithWebIdentityParams } from "@aws-sdk/credential-provider-web-identity"; +import { CredentialProvider, Credentials } from "@aws-sdk/types"; +import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials"; + +import { AssumeRoleParams } from "./resolveAssumeRoleCredentials"; +import { resolveProfileData } from "./resolveProfileData"; + +export interface FromIniInit extends SourceProfileInit { + /** + * A function that returns a promise fulfilled with an MFA token code for + * the provided MFA Serial code. If a profile requires an MFA code and + * `mfaCodeProvider` is not a valid function, the credential provider + * promise will be rejected. + * + * @param mfaSerial The serial code of the MFA device specified. + */ + mfaCodeProvider?: (mfaSerial: string) => Promise; + + /** + * A function that assumes a role and returns a promise fulfilled with + * credentials for the assumed role. + * + * @param sourceCreds The credentials with which to assume a role. + * @param params + */ + roleAssumer?: (sourceCreds: Credentials, params: AssumeRoleParams) => Promise; + + /** + * A function that assumes a role with web identity and returns a promise fulfilled with + * credentials for the assumed role. + * + * @param sourceCreds The credentials with which to assume a role. + * @param params + */ + roleAssumerWithWebIdentity?: (params: AssumeRoleWithWebIdentityParams) => Promise; +} + +/** + * Creates a credential provider that will read from ini files and supports + * role assumption and multi-factor authentication. + */ +export const fromIni = + (init: FromIniInit = {}): CredentialProvider => + async () => { + const profiles = await parseKnownFiles(init); + return resolveProfileData(getMasterProfileName(init), profiles, init); + }; diff --git a/packages/credential-provider-ini/src/index.spec.ts b/packages/credential-provider-ini/src/index.spec.ts deleted file mode 100644 index acdcd6656ce6..000000000000 --- a/packages/credential-provider-ini/src/index.spec.ts +++ /dev/null @@ -1,1329 +0,0 @@ -import { fromEnv } from "@aws-sdk/credential-provider-env"; -import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; -import { fromSSO, isSsoProfile, validateSsoProfile } from "@aws-sdk/credential-provider-sso"; -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"; -import { ENV_PROFILE } from "@aws-sdk/util-credentials"; -import { join, sep } from "path"; - -import { AssumeRoleParams, fromIni } from "./"; - -jest.mock("fs", () => { - interface FsModule { - __addMatcher(toMatch: string, toReturn: string): void; - __clearMatchers(): void; - readFile: (path: string, encoding: string, cb: (err: Error | null, data?: string) => void) => void; - } - - const fs: FsModule = jest.genMockFromModule("fs"); - const matchers = new Map(); - - function readFile(path: string, encoding: string, callback: (err: Error | null, data?: string) => void): void { - if (matchers.has(path)) { - callback(null, matchers.get(path)); - return; - } - - callback(new Error("ENOENT: no such file or directory")); - } - - fs.__addMatcher = function (toMatch: string, toReturn: string): void { - matchers.set(toMatch, toReturn); - }; - fs.__clearMatchers = function (): void { - matchers.clear(); - }; - fs.readFile = readFile; - - return fs; -}); -import fs from "fs"; -const { __addMatcher, __clearMatchers } = fs as any; - -jest.mock("os", () => { - interface OsModule { - homedir: () => string; - } - - const os: OsModule = jest.genMockFromModule("os"); - const path = require("path"); - - os.homedir = () => path.sep + path.join("home", "user"); - - return os; -}); - -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"); - -jest.mock("@aws-sdk/credential-provider-sso"); - -const DEFAULT_CREDS = { - accessKeyId: "AKIAIOSFODNN7EXAMPLE", - secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", - sessionToken: "sessionToken", -}; - -const FOO_CREDS = { - accessKeyId: "foo", - secretAccessKey: "bar", - sessionToken: "baz", -}; - -const FIZZ_CREDS = { - accessKeyId: "fizz", - secretAccessKey: "buzz", - sessionToken: "pop", -}; - -const envAtLoadTime: { [key: string]: string | undefined } = [ - ENV_CONFIG_PATH, - ENV_CREDENTIALS_PATH, - ENV_PROFILE, - "HOME", - "USERPROFILE", - "HOMEPATH", - "HOMEDRIVE", -].reduce((envState: { [key: string]: string | undefined }, varName: string) => { - envState[varName] = process.env[varName]; - return envState; -}, {}); - -beforeEach(() => { - __clearMatchers(); - jest.clearAllMocks(); - Object.keys(envAtLoadTime).forEach((envKey) => { - delete process.env[envKey]; - }); -}); - -afterAll(() => { - __clearMatchers(); - Object.keys(envAtLoadTime).forEach((envKey) => { - if (envAtLoadTime[envKey] === undefined) { - delete process.env[envKey]; - } else { - process.env[envKey] = envAtLoadTime[envKey]; - } - }); -}); - -describe("fromIni", () => { - it("should flag a lack of credentials as a non-terminal error", () => { - return expect(fromIni()()).rejects.toMatchObject({ - message: "Profile default could not be found or parsed in shared credentials file.", - tryNextLink: true, - }); - }); - - describe("shared credentials file", () => { - const SIMPLE_CREDS_FILE = ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim(); - - const DEFAULT_PATH = join(homedir(), ".aws", "credentials"); - - it("should read credentials from ~/.aws/credentials", async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CREDS_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should read profile credentials from ~/.aws/credentials", async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CREDS_FILE); - - expect(await fromIni({ profile: "foo" })()).toEqual(FOO_CREDS); - }); - - it(`should read the profile specified in ${ENV_PROFILE}`, async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CREDS_FILE); - process.env[ENV_PROFILE] = "foo"; - - expect(await fromIni()()).toEqual(FOO_CREDS); - }); - - it("should read from a filepath if provided", async () => { - const customPath = join(homedir(), ".aws", "foo"); - __addMatcher(customPath, SIMPLE_CREDS_FILE); - - expect(await fromIni({ filepath: customPath })()).toEqual(DEFAULT_CREDS); - }); - - it(`should read from a filepath specified in ${ENV_CREDENTIALS_PATH}`, async () => { - process.env[ENV_CREDENTIALS_PATH] = join("foo", "bar", "baz"); - __addMatcher(process.env[ENV_CREDENTIALS_PATH], SIMPLE_CREDS_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer a provided filepath over one specified via environment variables", async () => { - process.env[ENV_CREDENTIALS_PATH] = join("foo", "bar", "baz"); - const customPath = join("fizz", "buzz", "pop"); - __addMatcher( - customPath, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - process.env[ENV_CREDENTIALS_PATH], - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni({ filepath: customPath })()).toEqual(DEFAULT_CREDS); - }); - - it("should use $HOME when available", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - __addMatcher(`${sep}foo${sep}bar${sep}.aws${sep}credentials`, SIMPLE_CREDS_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should use $USERPROFILE when available", async () => { - process.env.USERPROFILE = "C:\\Users\\user"; - __addMatcher(`C:\\Users\\user${sep}.aws${sep}credentials`, SIMPLE_CREDS_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should use $HOMEPATH/$HOMEDRIVE when available", async () => { - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user"; - __addMatcher(`D:\\Users\\user${sep}.aws${sep}credentials`, SIMPLE_CREDS_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $HOME to $USERPROFILE", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - process.env.USERPROFILE = "C:\\Users\\user"; - - __addMatcher( - `${sep}foo${sep}bar${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `C:\\Users\\user${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $USERPROFILE to $HOMEDRIVE+$HOMEPATH", async () => { - process.env.USERPROFILE = "C:\\Users\\user"; - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user2"; - - __addMatcher( - `C:\\Users\\user${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `D:\\Users\\user2${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $HOME to $HOMEDRIVE+$HOMEPATH", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user2"; - - __addMatcher( - `${sep}foo${sep}bar${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `D:\\Users\\user2${sep}.aws${sep}credentials`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - }); - - describe("shared config file", () => { - const SIMPLE_CONFIG_FILE = ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[profile foo] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim(); - - const DEFAULT_PATH = join(homedir(), ".aws", "config"); - - it("should read credentials from ~/.aws/config", async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CONFIG_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should read profile credentials from ~/.aws/config", async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CONFIG_FILE); - - expect(await fromIni({ profile: "foo" })()).toEqual(FOO_CREDS); - }); - - it(`should read the profile specified in ${ENV_PROFILE}`, async () => { - __addMatcher(DEFAULT_PATH, SIMPLE_CONFIG_FILE); - process.env[ENV_PROFILE] = "foo"; - - expect(await fromIni()()).toEqual(FOO_CREDS); - }); - - it("should read from a filepath if provided", async () => { - const customPath = join(homedir(), ".aws", "foo"); - __addMatcher(customPath, SIMPLE_CONFIG_FILE); - - expect(await fromIni({ configFilepath: customPath })()).toEqual(DEFAULT_CREDS); - }); - - it(`should read from a filepath specified in ${ENV_CREDENTIALS_PATH}`, async () => { - process.env[ENV_CONFIG_PATH] = join("foo", "bar", "baz"); - __addMatcher(process.env[ENV_CONFIG_PATH], SIMPLE_CONFIG_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer a provided filepath over one specified via environment variables", async () => { - process.env[ENV_CONFIG_PATH] = join("foo", "bar", "baz"); - const customPath = join("fizz", "buzz", "pop"); - __addMatcher( - customPath, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - process.env[ENV_CONFIG_PATH], - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni({ configFilepath: customPath })()).toEqual(DEFAULT_CREDS); - }); - - it("should use $HOME when available", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - __addMatcher(`${sep}foo${sep}bar${sep}.aws${sep}config`, SIMPLE_CONFIG_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should use $USERPROFILE when available", async () => { - process.env.USERPROFILE = "C:\\Users\\user"; - __addMatcher(`C:\\Users\\user${sep}.aws${sep}config`, SIMPLE_CONFIG_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should use $HOMEPATH/$HOMEDRIVE when available", async () => { - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user"; - __addMatcher(`D:\\Users\\user${sep}.aws${sep}config`, SIMPLE_CONFIG_FILE); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $HOME to $USERPROFILE", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - process.env.USERPROFILE = "C:\\Users\\user"; - - __addMatcher( - `${sep}foo${sep}bar${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `C:\\Users\\user${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $USERPROFILE to $HOMEDRIVE+$HOMEPATH", async () => { - process.env.USERPROFILE = "C:\\Users\\user"; - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user2"; - - __addMatcher( - `C:\\Users\\user${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `D:\\Users\\user2${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should prefer $HOME to $HOMEDRIVE+$HOMEPATH", async () => { - process.env.HOME = `${sep}foo${sep}bar`; - process.env.HOMEDRIVE = "D:\\"; - process.env.HOMEPATH = "Users\\user2"; - - __addMatcher( - `${sep}foo${sep}bar${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - `D:\\Users\\user2${sep}.aws${sep}config`, - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - }); - - describe("assume role", () => { - it("should invoke a role assumer callback with credentials from a source profile", async () => { - const roleArn = "arn:aws:iam::123456789:role/foo"; - const sessionName = "fooSession"; - const externalId = "externalId"; - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = ${roleArn} -role_session_name = ${sessionName} -external_id = ${externalId} -source_profile = default`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(roleArn); - expect(params.RoleSessionName).toEqual(sessionName); - expect(params.ExternalId).toEqual(externalId); - - return Promise.resolve(FOO_CREDS); - }, - }); - - expect(await provider()).toEqual(FOO_CREDS); - }); - - it("should create a role session name if none provided", async () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = arn:aws:iam::123456789:role/foo -source_profile = default`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(params.RoleSessionName).toBeDefined(); - - return Promise.resolve(FOO_CREDS); - }, - }); - - expect(await provider()).toEqual(FOO_CREDS); - }); - - it("should reject the promise with a terminal error if no role assumer provided", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = arn:aws:iam::123456789:role/foo -source_profile = bar`.trim() - ); - - return expect(fromIni({ profile: "foo" })()).rejects.toMatchObject({ - message: "Profile foo requires a role to be assumed, but no role assumption callback was provided.", - tryNextLink: false, - }); - }); - - it("should reject the promise if the source profile cannot be found", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = arn:aws:iam::123456789:role/foo -source_profile = bar`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer: jest.fn(), - }); - - return expect(provider()).rejects.toMatchObject({ - message: "Profile bar could not be found or parsed in shared credentials file.", - tryNextLink: true, - }); - }); - - it("should allow a profile in ~/.aws/credentials to use a source profile from ~/.aws/config", async () => { - const roleArn = "arn:aws:iam::123456789:role/foo"; - - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[foo] -role_arn = ${roleArn} -source_profile = bar`.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[profile bar] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(roleArn); - - return Promise.resolve(FOO_CREDS); - }, - }); - - expect(await provider()).toEqual(FOO_CREDS); - }); - - it("should allow a profile in ~/.aws/config to use a source profile from ~/.aws/credentials", async () => { - const roleArn = "arn:aws:iam::123456789:role/foo"; - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[profile foo] -role_arn = ${roleArn} -source_profile = bar`.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[bar] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(roleArn); - - return Promise.resolve(FOO_CREDS); - }, - }); - - expect(await provider()).toEqual(FOO_CREDS); - }); - - it("should allow profiles to assume roles assuming roles assuming roles ad infinitum", async () => { - const roleArnFor = (profile: string) => `arn:aws:iam::123456789:role/${profile}`; - const roleAssumer = jest.fn(); - roleAssumer.mockReturnValue(Promise.resolve(FOO_CREDS)); - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[profile foo] -role_arn = ${roleArnFor("foo")} -source_profile = fizz - -[profile bar] -role_arn = ${roleArnFor("bar")} -source_profile = buzz - -[profile baz] -role_arn = ${roleArnFor("baz")} -source_profile = pop -`.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[fizz] -role_arn = ${roleArnFor("fizz")} -source_profile = bar - -[buzz] -role_arn = ${roleArnFor("buzz")} -source_profile = baz - -[pop] -role_arn = ${roleArnFor("pop")} -source_profile = default -`.trim() - ); - - expect(await fromIni({ roleAssumer, profile: "foo" })()).toEqual(FOO_CREDS); - - expect(roleAssumer.mock.calls.length).toEqual(6); - const expectedCalls = [ - { creds: DEFAULT_CREDS, arn: roleArnFor("pop") }, - { creds: FOO_CREDS, arn: roleArnFor("baz") }, - { creds: FOO_CREDS, arn: roleArnFor("buzz") }, - { creds: FOO_CREDS, arn: roleArnFor("bar") }, - { creds: FOO_CREDS, arn: roleArnFor("fizz") }, - { creds: FOO_CREDS, arn: roleArnFor("foo") }, - ]; - - for (const { creds, arn } of expectedCalls) { - const call = roleAssumer.mock.calls.shift(); - expect(call[0]).toEqual(creds); - expect(call[1].RoleArn).toEqual(arn); - } - }); - - it("should support assuming a role with multi-factor authentication", async () => { - const roleArn = "arn:aws:iam::123456789:role/foo"; - const mfaSerial = "mfaSerial"; - const mfaCode = Date.now().toString(10); - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = ${roleArn} -mfa_serial = ${mfaSerial} -source_profile = default`.trim() - ); - - const provider = fromIni({ - mfaCodeProvider() { - return Promise.resolve(mfaCode); - }, - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(roleArn); - expect(params.SerialNumber).toEqual(mfaSerial); - expect(params.TokenCode).toEqual(mfaCode); - - return Promise.resolve(FOO_CREDS); - }, - }); - - expect(await provider()).toEqual(FOO_CREDS); - }); - - it("should reject the promise with a terminal error if a MFA serial is present but no mfaCodeProvider was provided", () => { - const roleArn = "arn:aws:iam::123456789:role/foo"; - const mfaSerial = "mfaSerial"; - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - -[foo] -role_arn = ${roleArn} -mfa_serial = ${mfaSerial} -source_profile = default`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer: () => Promise.resolve(FOO_CREDS), - }); - - return expect(provider()).rejects.toMatchObject({ - message: "Profile foo requires multi-factor authentication, but no MFA code callback was provided.", - 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}, expected 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", () => { - it("should call fromTokenFile with data from profile", async () => { - (fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS)); - const webIdentityTokenFile = "/temp/foo/token"; - const roleArn = "arn:aws:iam::123456789:role/bar"; - const roleSessionName = "bazSession"; - const roleAssumerWithWebIdentity = jest.fn(); - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[foo] -web_identity_token_file = ${webIdentityTokenFile} -role_arn = ${roleArn} -role_session_name = ${roleSessionName}`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumerWithWebIdentity, - }); - - expect(await provider()).toEqual(FOO_CREDS); - expect(fromTokenFile).toHaveBeenCalledTimes(1); - expect(fromTokenFile).toHaveBeenCalledWith({ - webIdentityTokenFile, - roleArn, - roleSessionName, - roleAssumerWithWebIdentity, - }); - }); - - it("should call fromTokenFile with assume role chaining", async () => { - (fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(DEFAULT_CREDS)); - const webIdentityTokenFile = "/temp/foo/token"; - const roleArn = "arn:aws:iam::123456789:role/bar"; - const roleSessionName = "bazSession"; - const roleAssumerWithWebIdentity = jest.fn(); - - const fooRoleArn = "arn:aws:iam::123456789:role/foo"; - const fooSessionName = "fooSession"; - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[bar] -web_identity_token_file = ${webIdentityTokenFile} -role_arn = ${roleArn} -role_session_name = ${roleSessionName} - -[foo] -role_arn = ${fooRoleArn} -role_session_name = ${fooSessionName} -source_profile = bar`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(fooRoleArn); - expect(params.RoleSessionName).toEqual(fooSessionName); - return Promise.resolve(FOO_CREDS); - }, - roleAssumerWithWebIdentity, - }); - - expect(await provider()).toEqual(FOO_CREDS); - expect(fromTokenFile).toHaveBeenCalledTimes(1); - expect(fromTokenFile).toHaveBeenCalledWith({ - webIdentityTokenFile, - roleArn, - roleSessionName, - roleAssumerWithWebIdentity, - }); - }); - - it("should call fromTokenFile without roleSessionName if not present in profile", async () => { - (fromTokenFile as jest.Mock).mockReturnValueOnce(() => Promise.resolve(FOO_CREDS)); - const webIdentityTokenFile = "/temp/foo/token"; - const roleArn = "arn:aws:iam::123456789:role/bar"; - const roleAssumerWithWebIdentity = jest.fn(); - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[foo] -web_identity_token_file = ${webIdentityTokenFile} -role_arn = ${roleArn}`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumerWithWebIdentity, - }); - - expect(await provider()).toEqual(FOO_CREDS); - expect(fromTokenFile).toHaveBeenCalledTimes(1); - expect(fromTokenFile).toHaveBeenCalledWith({ - webIdentityTokenFile, - roleArn, - roleAssumerWithWebIdentity, - }); - }); - }); - - describe("assume role with SSO", () => { - const DEFAULT_PATH = join(homedir(), ".aws", "credentials"); - it("should continue if profile is not configured with an SSO credential", async () => { - __addMatcher( - DEFAULT_PATH, - `[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} - `.trim() - ); - await fromIni()(); - expect(fromSSO).not.toHaveBeenCalled(); - }); - - it("should throw if profile is configured with incomplete SSO credential", async () => { - (isSsoProfile as unknown as jest.Mock).mockImplementationOnce(() => true); - const originalValidator = jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile; - (validateSsoProfile as unknown as jest.Mock).mockImplementationOnce(originalValidator); - __addMatcher( - DEFAULT_PATH, - `[default] -sso_account_id = 1234567890 -sso_start_url = https://example.com/sso/ - `.trim() - ); - try { - await fromIni()(); - } catch (e) { - console.error(e.message); - expect(e.message).toEqual(expect.stringContaining("Profile is configured with invalid SSO credentials")); - } - }); - - it("should resolve valid SSO credential", async () => { - (isSsoProfile as unknown as jest.Mock).mockImplementationOnce(() => true); - const originalValidator = jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile; - (validateSsoProfile as jest.Mock).mockImplementationOnce(originalValidator); - (fromSSO as jest.Mock).mockImplementationOnce(() => async () => DEFAULT_CREDS); - const accountId = "1234567890"; - const startUrl = "https://example.com/sso/"; - const region = "us-east-1"; - const roleName = "role"; - __addMatcher( - DEFAULT_PATH, - `[default] -sso_account_id = ${accountId} -sso_start_url = ${startUrl} -sso_region = ${region} -sso_role_name = ${roleName} - `.trim() - ); - await fromIni()(); - expect(fromSSO as unknown as jest.Mock).toBeCalledWith({ - ssoAccountId: accountId, - ssoStartUrl: startUrl, - ssoRegion: region, - ssoRoleName: roleName, - }); - }); - - it("should call fromTokenFile with assume role chaining", async () => { - (isSsoProfile as unknown as jest.Mock).mockImplementationOnce( - jest.requireActual("@aws-sdk/credential-provider-sso").isSsoProfile - ); - (validateSsoProfile as unknown as jest.Mock).mockImplementationOnce( - jest.requireActual("@aws-sdk/credential-provider-sso").validateSsoProfile - ); - (fromSSO as jest.Mock).mockImplementationOnce(() => async () => DEFAULT_CREDS); - const accountId = "1234567890"; - const startUrl = "https://example.com/sso/"; - const region = "us-east-1"; - const roleName = "role"; - const roleAssumerWithWebIdentity = jest.fn(); - - const fooRoleArn = "arn:aws:iam::123456789:role/foo"; - const fooSessionName = "fooSession"; - __addMatcher( - DEFAULT_PATH, - ` -[bar] -sso_account_id = ${accountId} -sso_start_url = ${startUrl} -sso_region = ${region} -sso_role_name = ${roleName} - -[foo] -role_arn = ${fooRoleArn} -role_session_name = ${fooSessionName} -source_profile = bar`.trim() - ); - - const provider = fromIni({ - profile: "foo", - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual(fooRoleArn); - expect(params.RoleSessionName).toEqual(fooSessionName); - return Promise.resolve(FOO_CREDS); - }, - roleAssumerWithWebIdentity, - }); - - expect(await provider()).toEqual(FOO_CREDS); - expect(fromSSO).toHaveBeenCalledTimes(1); - expect(fromSSO).toHaveBeenCalledWith({ - ssoAccountId: accountId, - ssoStartUrl: startUrl, - ssoRegion: region, - ssoRoleName: roleName, - }); - }); - }); - - it("should prefer credentials in ~/.aws/credentials to those in ~/.aws/config", async () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken}`.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[default] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken}`.trim() - ); - - expect(await fromIni()()).toEqual(DEFAULT_CREDS); - }); - - it("should reject credentials with no access key", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} - `.trim() - ); - - return expect(fromIni()()).rejects.toMatchObject({ - message: "Profile default could not be found or parsed in shared credentials file.", - tryNextLink: true, - }); - }); - - it("should reject credentials with no secret key", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} - `.trim() - ); - - return expect(fromIni()()).rejects.toMatchObject({ - message: "Profile default could not be found or parsed in shared credentials file.", - tryNextLink: true, - }); - }); - - it("should not merge profile values together", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} - `.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[default] -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} - `.trim() - ); - - return expect(fromIni()()).rejects.toMatchObject({ - message: "Profile default could not be found or parsed in shared credentials file.", - tryNextLink: true, - }); - }); - - it("should treat a profile with static credentials and role assumption keys as an assume role profile on the first lookup", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -role_arn = foo -source_profile = foo - -[foo] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken} - `.trim() - ); - - const provider = fromIni({ - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(FOO_CREDS); - expect(params.RoleArn).toEqual("foo"); - - return Promise.resolve(FIZZ_CREDS); - }, - }); - - return expect(provider()).resolves.toEqual(FIZZ_CREDS); - }); - - it("should treat a profile with static credentials and role assumption keys as a static credentials profile on subsequent links", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -role_arn = foo -source_profile = foo - -[foo] -aws_access_key_id = ${FOO_CREDS.accessKeyId} -aws_secret_access_key = ${FOO_CREDS.secretAccessKey} -aws_session_token = ${FOO_CREDS.sessionToken} -role_arn = bar -source_profile = bar - -[bar] -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} - `.trim() - ); - - const provider = fromIni({ - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(FOO_CREDS); - expect(params.RoleArn).toEqual("foo"); - - return Promise.resolve(FIZZ_CREDS); - }, - }); - - return expect(provider()).resolves.toEqual(FIZZ_CREDS); - }); - - it("a profile should be able to use its own static creds to assume a role", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -aws_access_key_id = ${DEFAULT_CREDS.accessKeyId} -aws_secret_access_key = ${DEFAULT_CREDS.secretAccessKey} -aws_session_token = ${DEFAULT_CREDS.sessionToken} -role_arn = bar -source_profile = default - `.trim() - ); - - const provider = fromIni({ - roleAssumer(sourceCreds: Credentials, params: AssumeRoleParams): Promise { - expect(sourceCreds).toEqual(DEFAULT_CREDS); - expect(params.RoleArn).toEqual("bar"); - - return Promise.resolve(FOO_CREDS); - }, - }); - - return expect(provider()).resolves.toEqual(FOO_CREDS); - }); - - it("should reject credentials when profile role assumption creates a cycle", () => { - __addMatcher( - join(homedir(), ".aws", "credentials"), - ` -[default] -role_arn = foo -source_profile = foo - -[bar] -role_arn = baz -source_profile = baz - -[fizz] -role_arn = buzz -source_profile = foo - `.trim() - ); - - __addMatcher( - join(homedir(), ".aws", "config"), - ` -[profile foo] -role_arn = bar -source_profile = bar - -[profile baz] -role_arn = fizz -source_profile = fizz - `.trim() - ); - const provider = fromIni({ roleAssumer: jest.fn() }); - - return expect(provider()).rejects.toMatchObject({ - message: - "Detected a cycle attempting to resolve credentials for profile default. Profiles visited: foo, bar, baz, fizz", - tryNextLink: false, - }); - }); - - it("should not attempt to load from disk if loaded credentials are provided", async () => { - await expect(fromIni()()).rejects.toMatchObject({ - message: "Profile default could not be found or parsed in shared credentials file.", - }); - - const params = { - loadedConfig: Promise.resolve({ - configFile: { - default: { - aws_access_key_id: DEFAULT_CREDS.accessKeyId, - aws_secret_access_key: DEFAULT_CREDS.secretAccessKey, - aws_session_token: DEFAULT_CREDS.sessionToken, - }, - }, - credentialsFile: {}, - }), - }; - expect(await fromIni(params)()).toEqual(DEFAULT_CREDS); - }); -}); diff --git a/packages/credential-provider-ini/src/index.ts b/packages/credential-provider-ini/src/index.ts index 968c4bb0796f..b01913159b0b 100755 --- a/packages/credential-provider-ini/src/index.ts +++ b/packages/credential-provider-ini/src/index.ts @@ -1,266 +1 @@ -import { fromEnv } from "@aws-sdk/credential-provider-env"; -import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; -import { fromSSO, isSsoProfile, validateSsoProfile } from "@aws-sdk/credential-provider-sso"; -import { AssumeRoleWithWebIdentityParams, fromTokenFile } from "@aws-sdk/credential-provider-web-identity"; -import { CredentialsProviderError } from "@aws-sdk/property-provider"; -import { ParsedIniData, Profile } from "@aws-sdk/shared-ini-file-loader"; -import { CredentialProvider, Credentials } from "@aws-sdk/types"; -import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials"; - -/** - * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html#assumeRole-property - * TODO update the above to link to V3 docs - */ -export interface AssumeRoleParams { - /** - * The identifier of the role to be assumed. - */ - RoleArn: string; - - /** - * A name for the assumed role session. - */ - RoleSessionName: string; - - /** - * A unique identifier that is used by third parties when assuming roles in - * their customers' accounts. - */ - ExternalId?: string; - - /** - * The identification number of the MFA device that is associated with the - * user who is making the `AssumeRole` call. - */ - SerialNumber?: string; - - /** - * The value provided by the MFA device. - */ - TokenCode?: string; -} - -export interface FromIniInit extends SourceProfileInit { - /** - * A function that returns a promise fulfilled with an MFA token code for - * the provided MFA Serial code. If a profile requires an MFA code and - * `mfaCodeProvider` is not a valid function, the credential provider - * promise will be rejected. - * - * @param mfaSerial The serial code of the MFA device specified. - */ - mfaCodeProvider?: (mfaSerial: string) => Promise; - - /** - * A function that assumes a role and returns a promise fulfilled with - * credentials for the assumed role. - * - * @param sourceCreds The credentials with which to assume a role. - * @param params - */ - roleAssumer?: (sourceCreds: Credentials, params: AssumeRoleParams) => Promise; - - /** - * A function that assumes a role with web identity and returns a promise fulfilled with - * credentials for the assumed role. - * - * @param sourceCreds The credentials with which to assume a role. - * @param params - */ - roleAssumerWithWebIdentity?: (params: AssumeRoleWithWebIdentityParams) => Promise; -} - -interface StaticCredsProfile extends Profile { - aws_access_key_id: string; - aws_secret_access_key: string; - aws_session_token?: string; -} - -const isStaticCredsProfile = (arg: any): arg is StaticCredsProfile => - Boolean(arg) && - typeof arg === "object" && - typeof arg.aws_access_key_id === "string" && - typeof arg.aws_secret_access_key === "string" && - ["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1; - -interface WebIdentityProfile extends Profile { - web_identity_token_file: string; - role_arn: string; - role_session_name?: string; -} - -const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile => - Boolean(arg) && - typeof arg === "object" && - typeof arg.web_identity_token_file === "string" && - typeof arg.role_arn === "string" && - ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1; - -interface AssumeRoleWithSourceProfile extends Profile { - role_arn: string; - source_profile: string; -} - -interface AssumeRoleWithProviderProfile extends Profile { - role_arn: string; - credential_source: string; -} - -const isAssumeRoleProfile = (arg: any) => - Boolean(arg) && - typeof arg === "object" && - typeof arg.role_arn === "string" && - ["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 => - isAssumeRoleProfile(arg) && typeof arg.credential_source === "string" && typeof arg.source_profile === "undefined"; - -/** - * Creates a credential provider that will read from ini files and supports - * role assumption and multi-factor authentication. - */ -export const fromIni = - (init: FromIniInit = {}): CredentialProvider => - async () => { - const profiles = await parseKnownFiles(init); - return resolveProfileData(getMasterProfileName(init), profiles, init); - }; - -const resolveProfileData = async ( - profileName: string, - profiles: ParsedIniData, - options: FromIniInit, - visitedProfiles: { [profileName: string]: true } = {} -): Promise => { - const data = profiles[profileName]; - - // If this is not the first profile visited, static credentials should be - // preferred over role assumption metadata. This special treatment of - // second and subsequent hops is to ensure compatibility with the AWS CLI. - if (Object.keys(visitedProfiles).length > 0 && isStaticCredsProfile(data)) { - return resolveStaticCredentials(data); - } - - // If this is the first profile visited, role assumption keys should be - // given precedence over static credentials. - 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) { - throw new CredentialsProviderError( - `Profile ${profileName} requires a role to be assumed, but no` + ` role assumption callback was provided.`, - false - ); - } - - if (source_profile && source_profile in visitedProfiles) { - throw new CredentialsProviderError( - `Detected a cycle attempting to resolve credentials for profile` + - ` ${getMasterProfileName(options)}. Profiles visited: ` + - Object.keys(visitedProfiles).join(", "), - false - ); - } - - 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) { - throw new CredentialsProviderError( - `Profile ${profileName} requires multi-factor authentication,` + ` but no MFA code callback was provided.`, - false - ); - } - params.SerialNumber = mfa_serial; - params.TokenCode = await options.mfaCodeProvider(mfa_serial); - } - - return options.roleAssumer(await sourceCreds, params); - } - - // If no role assumption metadata is present, attempt to load static - // credentials from the selected profile. - if (isStaticCredsProfile(data)) { - return resolveStaticCredentials(data); - } - - // If no static credentials are present, attempt to assume role with - // web identity if web_identity_token_file and role_arn is available - if (isWebIdentityProfile(data)) { - return resolveWebIdentityCredentials(data, options); - } - if (isSsoProfile(data)) { - const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(data); - return fromSSO({ - ssoStartUrl: sso_start_url, - ssoAccountId: sso_account_id, - ssoRegion: sso_region, - ssoRoleName: sso_role_name, - })(); - } - - // If the profile cannot be parsed or contains neither static credentials - // nor role assumption metadata, throw an error. This should be considered a - // terminal resolution error if a profile has been specified by the user - // (whether via a parameter, an environment variable, or another profile's - // `source_profile` key). - throw new CredentialsProviderError( - `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 CredentialsProviderError( - `Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` + - `expected EcsContainer or Ec2InstanceMetadata or Environment.` - ); - } -}; - -const resolveStaticCredentials = (profile: StaticCredsProfile): Promise => - Promise.resolve({ - accessKeyId: profile.aws_access_key_id, - secretAccessKey: profile.aws_secret_access_key, - sessionToken: profile.aws_session_token, - }); - -const resolveWebIdentityCredentials = async (profile: WebIdentityProfile, options: FromIniInit): Promise => - fromTokenFile({ - webIdentityTokenFile: profile.web_identity_token_file, - roleArn: profile.role_arn, - roleSessionName: profile.role_session_name, - roleAssumerWithWebIdentity: options.roleAssumerWithWebIdentity, - })(); +export * from "./fromIni"; diff --git a/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.spec.ts b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.spec.ts new file mode 100644 index 000000000000..3074dbc8c8a9 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.spec.ts @@ -0,0 +1,258 @@ +import { CredentialsProviderError } from "@aws-sdk/property-provider"; +import { getMasterProfileName } from "@aws-sdk/util-credentials"; + +import { isAssumeRoleProfile, resolveAssumeRoleCredentials } from "./resolveAssumeRoleCredentials"; +import { resolveCredentialSource } from "./resolveCredentialSource"; +import { resolveProfileData } from "./resolveProfileData"; + +jest.mock("@aws-sdk/util-credentials"); +jest.mock("./resolveCredentialSource"); +jest.mock("./resolveProfileData"); + +const getMockAssumeRoleProfile = () => ({ + role_arn: "mock_role_arn", + role_session_name: "mock_role_session_name", + external_id: "mock_external_id", + mfa_serial: "mock_mfa_serial", +}); + +const getMockSourceProfileKeyValues = () => ({ + source_profile: "mock_source_profile", +}); + +const getMockProviderProfileKeyValues = () => ({ + credential_source: "mock_credential_source", +}); + +describe(isAssumeRoleProfile.name, () => { + describe("returns false for falsy values", () => { + it.each([false, 0, -0, "", null, undefined, NaN])("%s:", (falsyValue) => { + expect(isAssumeRoleProfile(falsyValue)).toEqual(false); + }); + }); + + describe("returns false for data type which is not an object", () => { + it.each([true, 1, "string"])("%s:", (notObject) => { + expect(isAssumeRoleProfile(notObject)).toEqual(false); + }); + }); + + it.each(["role_arn"])("value at '%s' is not of type string", (key) => { + [true, null, undefined, 1, NaN, {}].forEach((value) => { + expect( + isAssumeRoleProfile({ + ...getMockAssumeRoleProfile(), + ...getMockSourceProfileKeyValues(), + ...getMockProviderProfileKeyValues(), + [key]: value, + }) + ).toEqual(false); + }); + }); + + it.each(["role_session_name", "external_id", "mfa_serial"])( + "value at '%s' is not of type string | undefined", + (key) => { + [true, null, 1, NaN, {}].forEach((value) => { + expect( + isAssumeRoleProfile({ + ...getMockAssumeRoleProfile(), + ...getMockSourceProfileKeyValues(), + ...getMockProviderProfileKeyValues(), + [key]: value, + }) + ).toEqual(false); + }); + } + ); + + it("returns true for AssumeRoleWithSourceProfile", () => { + expect(isAssumeRoleProfile({ ...getMockAssumeRoleProfile(), ...getMockSourceProfileKeyValues() })).toEqual(true); + }); + + it("returns true for AssumeRoleWithProviderProfile", () => { + expect(isAssumeRoleProfile({ ...getMockAssumeRoleProfile(), ...getMockProviderProfileKeyValues() })).toEqual(true); + }); +}); + +describe(resolveAssumeRoleCredentials.name, () => { + const mockCreds = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + const mockSourceCredsFromProfile = { + accessKeyId: "mockProfileAccessKeyId", + secretAccessKey: "mockProfileSecretAccessKey", + }; + const mockSourceCredsFromCredential = { + accessKeyId: "mockCredsAccessKeyId", + secretAccessKey: "mockCredsSecretAccessKey", + }; + + const mockProfileName = "mockProfileName"; + const mockProfiles = { [mockProfileName]: {} }; + const mockOptions = { + mfaCodeProvider: jest.fn(), + roleAssumer: jest.fn().mockReturnValue(mockCreds), + roleAssumerWithWebIdentity: jest.fn(), + }; + const mockCredentialSource = "mockCredentialSource"; + + const getMockRoleAssumeParams = () => ({ + role_arn: "mock_role_arn", + role_session_name: "mock_role_session_name", + external_id: "mock_external_id", + }); + + const getMockProfilesWithCredSource = (additionalData) => ({ + [mockProfileName]: { + credential_source: mockCredentialSource, + ...additionalData, + }, + }); + + beforeEach(() => { + (getMasterProfileName as jest.Mock).mockReturnValue(mockProfileName); + (resolveProfileData as jest.Mock).mockResolvedValue(mockSourceCredsFromProfile); + (resolveCredentialSource as jest.Mock).mockReturnValue(() => Promise.resolve(mockSourceCredsFromCredential)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("throws error if roleAssumer is not available in options", async () => { + const expectedError = new CredentialsProviderError( + `Profile ${mockProfileName} requires a role to be assumed, but no role assumption callback was provided.`, + false + ); + try { + await resolveAssumeRoleCredentials(mockProfileName, mockProfiles, { ...mockOptions, roleAssumer: undefined }); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + }); + + it("throws error if source_profile is in visited profiles", async () => { + const mockProfileCurrent = "mockProfileCurrent"; + const mockProfilesWithCycle = { [mockProfileCurrent]: { source_profile: mockProfileName }, [mockProfileName]: {} }; + + const expectedError = new CredentialsProviderError( + `Detected a cycle attempting to resolve credentials for profile` + + ` ${mockProfileName}. Profiles visited: ` + + Object.keys({ mockProfileName: true }).join(", "), + false + ); + + try { + await resolveAssumeRoleCredentials(mockProfileCurrent, mockProfilesWithCycle, mockOptions, { + mockProfileName: true, + }); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + }); + + it("gets sourceCreds from Profile data if source_profile is present", async () => { + const mockProfileCurrent = "mockProfileCurrent"; + const mockRoleAssumeParams = getMockRoleAssumeParams(); + const mockProfilesWithSource = { + [mockProfileCurrent]: { + source_profile: mockProfileName, + ...mockRoleAssumeParams, + }, + [mockProfileName]: {}, + }; + + const receivedCreds = await resolveAssumeRoleCredentials(mockProfileCurrent, mockProfilesWithSource, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveProfileData).toHaveBeenCalledWith(mockProfileName, mockProfilesWithSource, mockOptions, { + mockProfileName: true, + }); + expect(resolveCredentialSource).not.toHaveBeenCalled(); + expect(mockOptions.roleAssumer).toHaveBeenCalledWith(mockSourceCredsFromProfile, { + RoleArn: mockRoleAssumeParams.role_arn, + RoleSessionName: mockRoleAssumeParams.role_session_name, + ExternalId: mockRoleAssumeParams.external_id, + }); + }); + + it("gets sourceCreds from Credentials Source if source_profile is absent", async () => { + const mockRoleAssumeParams = getMockRoleAssumeParams(); + const mockProfilesWithCredSource = getMockProfilesWithCredSource(mockRoleAssumeParams); + + const receivedCreds = await resolveAssumeRoleCredentials(mockProfileName, mockProfilesWithCredSource, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveProfileData).not.toHaveBeenCalled(); + expect(resolveCredentialSource).toHaveBeenCalledWith(mockCredentialSource, mockProfileName); + expect(mockOptions.roleAssumer).toHaveBeenCalledWith(mockSourceCredsFromCredential, { + RoleArn: mockRoleAssumeParams.role_arn, + RoleSessionName: mockRoleAssumeParams.role_session_name, + ExternalId: mockRoleAssumeParams.external_id, + }); + }); + + it("sets role session name if not provided", async () => { + const mockDateNow = Date.now(); + jest.spyOn(Date, "now").mockReturnValue(mockDateNow); + + const mockRoleAssumeParams = { ...getMockRoleAssumeParams(), role_session_name: undefined }; + const mockProfilesWithCredSource = getMockProfilesWithCredSource(mockRoleAssumeParams); + + const receivedCreds = await resolveAssumeRoleCredentials(mockProfileName, mockProfilesWithCredSource, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(mockOptions.roleAssumer).toHaveBeenCalledWith(mockSourceCredsFromCredential, { + RoleArn: mockRoleAssumeParams.role_arn, + RoleSessionName: `aws-sdk-js-${mockDateNow}`, + ExternalId: mockRoleAssumeParams.external_id, + }); + }); + + it("throws error is mfa_serial is provided", async () => { + const mockRoleAssumeParams = getMockRoleAssumeParams(); + const mockMfaSerial = "mock_mfa_serial"; + const mockProfilesWithCredSource = getMockProfilesWithCredSource({ + ...mockRoleAssumeParams, + mfa_serial: mockMfaSerial, + }); + + const expectedError = new CredentialsProviderError( + `Profile ${mockProfileName} requires multi-factor authentication, but no MFA code callback was provided.`, + false + ); + try { + await resolveAssumeRoleCredentials(mockProfileName, mockProfilesWithCredSource, { + ...mockOptions, + mfaCodeProvider: undefined, + }); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(mockOptions.roleAssumer).not.toHaveBeenCalled(); + }); + + it("populates SerialNumber and TokenCode based on mfa_serial", async () => { + const mockRoleAssumeParams = getMockRoleAssumeParams(); + const mockMfaSerial = "mock_mfa_serial"; + const mockProfilesWithCredSource = getMockProfilesWithCredSource({ + ...mockRoleAssumeParams, + mfa_serial: mockMfaSerial, + }); + + const mockTokenCode = "mockTokenCode"; + mockOptions.mfaCodeProvider.mockResolvedValue(mockTokenCode); + + const receivedCreds = await resolveAssumeRoleCredentials(mockProfileName, mockProfilesWithCredSource, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(mockOptions.roleAssumer).toHaveBeenCalledWith(mockSourceCredsFromCredential, { + RoleArn: mockRoleAssumeParams.role_arn, + RoleSessionName: mockRoleAssumeParams.role_session_name, + ExternalId: mockRoleAssumeParams.external_id, + SerialNumber: mockMfaSerial, + TokenCode: mockTokenCode, + }); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts new file mode 100644 index 000000000000..1ca1d22e4b94 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts @@ -0,0 +1,119 @@ +import { CredentialsProviderError } from "@aws-sdk/property-provider"; +import { ParsedIniData, Profile } from "@aws-sdk/shared-ini-file-loader"; +import { getMasterProfileName } from "@aws-sdk/util-credentials"; + +import { FromIniInit } from "./fromIni"; +import { resolveCredentialSource } from "./resolveCredentialSource"; +import { resolveProfileData } from "./resolveProfileData"; + +/** + * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html#assumeRole-property + * TODO update the above to link to V3 docs + */ +export interface AssumeRoleParams { + /** + * The identifier of the role to be assumed. + */ + RoleArn: string; + + /** + * A name for the assumed role session. + */ + RoleSessionName: string; + + /** + * A unique identifier that is used by third parties when assuming roles in + * their customers' accounts. + */ + ExternalId?: string; + + /** + * The identification number of the MFA device that is associated with the + * user who is making the `AssumeRole` call. + */ + SerialNumber?: string; + + /** + * The value provided by the MFA device. + */ + TokenCode?: string; +} + +interface AssumeRoleWithSourceProfile extends Profile { + role_arn: string; + source_profile: string; +} + +interface AssumeRoleWithProviderProfile extends Profile { + role_arn: string; + credential_source: string; +} + +export const isAssumeRoleProfile = (arg: any) => + Boolean(arg) && + typeof arg === "object" && + typeof arg.role_arn === "string" && + ["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 && + (isAssumeRoleWithSourceProfile(arg) || isAssumeRoleWithProviderProfile(arg)); + +const isAssumeRoleWithSourceProfile = (arg: any): arg is AssumeRoleWithSourceProfile => + typeof arg.source_profile === "string" && typeof arg.credential_source === "undefined"; + +const isAssumeRoleWithProviderProfile = (arg: any): arg is AssumeRoleWithProviderProfile => + typeof arg.credential_source === "string" && typeof arg.source_profile === "undefined"; + +export const resolveAssumeRoleCredentials = async ( + profileName: string, + profiles: ParsedIniData, + options: FromIniInit, + visitedProfiles: { [profileName: string]: true } = {} +) => { + const data = profiles[profileName]; + + if (!options.roleAssumer) { + throw new CredentialsProviderError( + `Profile ${profileName} requires a role to be assumed, but no role assumption callback was provided.`, + false + ); + } + + const { source_profile } = data; + if (source_profile && source_profile in visitedProfiles) { + throw new CredentialsProviderError( + `Detected a cycle attempting to resolve credentials for profile` + + ` ${getMasterProfileName(options)}. Profiles visited: ` + + Object.keys(visitedProfiles).join(", "), + false + ); + } + + const sourceCredsProvider = source_profile + ? resolveProfileData(source_profile, profiles, options, { + ...visitedProfiles, + [source_profile]: true, + }) + : resolveCredentialSource(data.credential_source!, profileName)(); + + const params: AssumeRoleParams = { + RoleArn: data.role_arn!, + RoleSessionName: data.role_session_name || `aws-sdk-js-${Date.now()}`, + ExternalId: data.external_id, + }; + + const { mfa_serial } = data; + if (mfa_serial) { + if (!options.mfaCodeProvider) { + throw new CredentialsProviderError( + `Profile ${profileName} requires multi-factor authentication, but no MFA code callback was provided.`, + false + ); + } + params.SerialNumber = mfa_serial; + params.TokenCode = await options.mfaCodeProvider(mfa_serial); + } + + const sourceCreds = await sourceCredsProvider; + return options.roleAssumer(sourceCreds, params); +}; diff --git a/packages/credential-provider-ini/src/resolveCredentialSource.spec.ts b/packages/credential-provider-ini/src/resolveCredentialSource.spec.ts new file mode 100644 index 000000000000..25c11c4be097 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveCredentialSource.spec.ts @@ -0,0 +1,65 @@ +import { fromEnv } from "@aws-sdk/credential-provider-env"; +import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; +import { CredentialsProviderError } from "@aws-sdk/property-provider"; + +import { resolveCredentialSource } from "./resolveCredentialSource"; + +jest.mock("@aws-sdk/credential-provider-env"); +jest.mock("@aws-sdk/credential-provider-imds"); + +describe(resolveCredentialSource.name, () => { + const mockProfileName = "mockProfileName"; + + const mockCreds = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + + const mockFakeCreds = { + accessKeyId: "mockFakeAccessKeyId", + secretAccessKey: "mockFakeSecretAccessKey", + }; + + beforeEach(() => { + (fromEnv as jest.Mock).mockReturnValue(() => Promise.resolve(mockFakeCreds)); + (fromContainerMetadata as jest.Mock).mockReturnValue(() => Promise.resolve(mockFakeCreds)); + (fromInstanceMetadata as jest.Mock).mockReturnValue(() => Promise.resolve(mockFakeCreds)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + ["EcsContainer", fromContainerMetadata], + ["Ec2InstanceMetadata", fromInstanceMetadata], + ["Environment", fromEnv], + ])("when credentialSource=%s, calls %p", async (credentialSource, fromFn) => { + (fromFn as jest.Mock).mockReturnValue(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveCredentialSource(credentialSource, mockProfileName)(); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(fromFn).toHaveBeenCalledWith(); + [fromContainerMetadata, fromInstanceMetadata, fromEnv] + .filter((fn) => fn !== fromFn) + .forEach((fnNotCalled) => { + expect(fnNotCalled).not.toHaveBeenCalled(); + }); + }); + + it("throws error if credentialSource is not one of ['EcsContainer','Ec2InstanceMetadata','Environment']", async () => { + const mockCredentialSource = "mockCredentialSource"; + const expectedError = new CredentialsProviderError( + `Unsupported credential source in profile ${mockProfileName}. Got ${mockCredentialSource}, ` + + `expected EcsContainer or Ec2InstanceMetadata or Environment.` + ); + try { + await resolveCredentialSource(mockCredentialSource, mockProfileName)(); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + [fromContainerMetadata, fromInstanceMetadata, fromEnv].forEach((fnNotCalled) => { + expect(fnNotCalled).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveCredentialSource.ts b/packages/credential-provider-ini/src/resolveCredentialSource.ts new file mode 100644 index 000000000000..f621967b13e8 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveCredentialSource.ts @@ -0,0 +1,27 @@ +import { fromEnv } from "@aws-sdk/credential-provider-env"; +import { fromContainerMetadata, fromInstanceMetadata } from "@aws-sdk/credential-provider-imds"; +import { CredentialsProviderError } from "@aws-sdk/property-provider"; +import { CredentialProvider } from "@aws-sdk/types"; + +/** + * 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. + */ +export 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 CredentialsProviderError( + `Unsupported credential source in profile ${profileName}. Got ${credentialSource}, ` + + `expected EcsContainer or Ec2InstanceMetadata or Environment.` + ); + } +}; diff --git a/packages/credential-provider-ini/src/resolveProfileData.spec.ts b/packages/credential-provider-ini/src/resolveProfileData.spec.ts new file mode 100644 index 000000000000..05f2ae54fd61 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveProfileData.spec.ts @@ -0,0 +1,122 @@ +import { CredentialsProviderError } from "@aws-sdk/property-provider"; + +import { isAssumeRoleProfile, resolveAssumeRoleCredentials } from "./resolveAssumeRoleCredentials"; +import { resolveProfileData } from "./resolveProfileData"; +import { isSsoProfile, resolveSsoCredentials } from "./resolveSsoCredentials"; +import { isStaticCredsProfile, resolveStaticCredentials } from "./resolveStaticCredentials"; +import { isWebIdentityProfile, resolveWebIdentityCredentials } from "./resolveWebIdentityCredentials"; + +jest.mock("./resolveAssumeRoleCredentials"); +jest.mock("./resolveSsoCredentials"); +jest.mock("./resolveStaticCredentials"); +jest.mock("./resolveWebIdentityCredentials"); + +describe(resolveProfileData.name, () => { + const mockProfileName = "mockProfileName"; + const mockProfiles = { [mockProfileName]: {} }; + const mockOptions = { + mfaCodeProvider: jest.fn(), + roleAssumer: jest.fn(), + roleAssumerWithWebIdentity: jest.fn(), + }; + const mockError = new CredentialsProviderError( + `Profile ${mockProfileName} could not be found or parsed in shared credentials file.` + ); + + const mockCreds = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + + const mockFakeCreds = { + accessKeyId: "mockFakeAccessKeyId", + secretAccessKey: "mockFakeSecretAccessKey", + }; + + beforeEach(() => { + [ + resolveAssumeRoleCredentials, + resolveSsoCredentials, + resolveStaticCredentials, + resolveWebIdentityCredentials, + ].forEach((resolveCredsFn) => { + (resolveCredsFn as jest.Mock).mockImplementation(() => Promise.resolve(mockFakeCreds)); + }); + }); + + beforeEach(() => { + [isAssumeRoleProfile, isSsoProfile, isStaticCredsProfile, isWebIdentityProfile].forEach((isProfileFn) => { + (isProfileFn as jest.Mock).mockReturnValue(true); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("throws error if all profile checks fail", async () => { + [isAssumeRoleProfile, isSsoProfile, isStaticCredsProfile, isWebIdentityProfile].forEach((isProfileFn) => { + (isProfileFn as jest.Mock).mockReturnValue(false); + }); + try { + await resolveProfileData(mockProfileName, mockProfiles, mockOptions); + fail(`expected ${mockError}`); + } catch (error) { + expect(error).toStrictEqual(mockError); + } + }); + + it("resolves with static creds when profiles are previously visited and current profile has static creds", async () => { + (resolveStaticCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions, { testProfile: true }); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveStaticCredentials).toHaveBeenCalledWith(mockProfiles[mockProfileName]); + }); + + describe("resolves with assumeRole", () => { + it("when it's static creds, but profiles are not visited", async () => { + (resolveAssumeRoleCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveAssumeRoleCredentials).toHaveBeenCalledWith(mockProfileName, mockProfiles, mockOptions, {}); + }); + + it("when it's not static creds, and profiles are visited", async () => { + (isStaticCredsProfile as unknown as jest.Mock).mockReturnValue(false); + (resolveAssumeRoleCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions, { testProfile: true }); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveAssumeRoleCredentials).toHaveBeenCalledWith(mockProfileName, mockProfiles, mockOptions, { + testProfile: true, + }); + }); + }); + + it("resolves with static creds when no profiles are visited and it's not assume role profile", async () => { + (isAssumeRoleProfile as unknown as jest.Mock).mockReturnValue(false); + (resolveStaticCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveStaticCredentials).toHaveBeenCalledWith(mockProfiles[mockProfileName]); + }); + + it("resolves with web identity profile, when it's not static or assume role", async () => { + [isAssumeRoleProfile, isStaticCredsProfile].forEach((isProfileFn) => { + (isProfileFn as jest.Mock).mockReturnValue(false); + }); + (resolveWebIdentityCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveWebIdentityCredentials).toHaveBeenCalledWith(mockProfiles[mockProfileName], mockOptions); + }); + + it("resolves with sso profile, when it's not static or assume role or web identity", async () => { + [isAssumeRoleProfile, isStaticCredsProfile, isWebIdentityProfile].forEach((isProfileFn) => { + (isProfileFn as jest.Mock).mockReturnValue(false); + }); + (resolveSsoCredentials as jest.Mock).mockImplementation(() => Promise.resolve(mockCreds)); + const receivedCreds = await resolveProfileData(mockProfileName, mockProfiles, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(resolveSsoCredentials).toHaveBeenCalledWith(mockProfiles[mockProfileName]); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveProfileData.ts b/packages/credential-provider-ini/src/resolveProfileData.ts new file mode 100644 index 000000000000..1590bd95381d --- /dev/null +++ b/packages/credential-provider-ini/src/resolveProfileData.ts @@ -0,0 +1,54 @@ +import { CredentialsProviderError } from "@aws-sdk/property-provider"; +import { ParsedIniData } from "@aws-sdk/shared-ini-file-loader"; +import { Credentials } from "@aws-sdk/types"; + +import { FromIniInit } from "./fromIni"; +import { isAssumeRoleProfile, resolveAssumeRoleCredentials } from "./resolveAssumeRoleCredentials"; +import { isSsoProfile, resolveSsoCredentials } from "./resolveSsoCredentials"; +import { isStaticCredsProfile, resolveStaticCredentials } from "./resolveStaticCredentials"; +import { isWebIdentityProfile, resolveWebIdentityCredentials } from "./resolveWebIdentityCredentials"; + +export const resolveProfileData = async ( + profileName: string, + profiles: ParsedIniData, + options: FromIniInit, + visitedProfiles: { [profileName: string]: true } = {} +): Promise => { + const data = profiles[profileName]; + + // If this is not the first profile visited, static credentials should be + // preferred over role assumption metadata. This special treatment of + // second and subsequent hops is to ensure compatibility with the AWS CLI. + if (Object.keys(visitedProfiles).length > 0 && isStaticCredsProfile(data)) { + return resolveStaticCredentials(data); + } + + // If this is the first profile visited, role assumption keys should be + // given precedence over static credentials. + if (isAssumeRoleProfile(data)) { + return resolveAssumeRoleCredentials(profileName, profiles, options, visitedProfiles); + } + + // If no role assumption metadata is present, attempt to load static + // credentials from the selected profile. + if (isStaticCredsProfile(data)) { + return resolveStaticCredentials(data); + } + + // If no static credentials are present, attempt to assume role with + // web identity if web_identity_token_file and role_arn is available + if (isWebIdentityProfile(data)) { + return resolveWebIdentityCredentials(data, options); + } + + if (isSsoProfile(data)) { + return resolveSsoCredentials(data); + } + + // If the profile cannot be parsed or contains neither static credentials + // nor role assumption metadata, throw an error. This should be considered a + // terminal resolution error if a profile has been specified by the user + // (whether via a parameter, an environment variable, or another profile's + // `source_profile` key). + throw new CredentialsProviderError(`Profile ${profileName} could not be found or parsed in shared credentials file.`); +}; diff --git a/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts new file mode 100644 index 000000000000..f22ff3e240a9 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts @@ -0,0 +1,98 @@ +import { fromSSO, isSsoProfile as origIsSsoProfile, validateSsoProfile } from "@aws-sdk/credential-provider-sso"; +import { Credentials } from "@aws-sdk/types"; + +import { isSsoProfile, resolveSsoCredentials } from "./resolveSsoCredentials"; + +jest.mock("@aws-sdk/credential-provider-sso"); + +describe(isSsoProfile.name, () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([false, true])("returns value returned by original isSsoProfile: %s", (value) => { + (origIsSsoProfile as unknown as jest.Mock).mockReturnValue(value); + expect(isSsoProfile({})).toEqual(value); + }); +}); + +describe(resolveSsoCredentials.name, () => { + const getMockOriginalSsoProfile = () => ({ + sso_start_url: "mock_sso_start_url", + sso_account_id: "mock_sso_account_id", + sso_region: "mock_sso_region", + sso_role_name: "mock_sso_role_name", + }); + + const getMockValidatedSsoProfile = () => ({ + sso_start_url: "mock_validated_sso_start_url", + sso_account_id: "mock_validated_sso_account_id", + sso_region: "mock_validated_sso_region", + sso_role_name: "mock_validated_sso_role_name", + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("throws error when validation fails", async () => { + const mockProfile = getMockOriginalSsoProfile(); + const expectedError = new Error("error from validateSsoProfile"); + (validateSsoProfile as jest.Mock).mockImplementation(() => { + throw expectedError; + }); + try { + await resolveSsoCredentials(mockProfile); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(validateSsoProfile).toHaveBeenCalledWith(mockProfile); + }); + + it("throws error when fromSSO throws error", async () => { + const mockProfile = getMockOriginalSsoProfile(); + const mockValidatedProfile = getMockValidatedSsoProfile(); + const expectedError = new Error("error from fromSSO"); + + (validateSsoProfile as jest.Mock).mockReturnValue(mockValidatedProfile); + (fromSSO as jest.Mock).mockReturnValue(() => Promise.reject(expectedError)); + + try { + await resolveSsoCredentials(mockProfile); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(validateSsoProfile).toHaveBeenCalledWith(mockProfile); + expect(fromSSO).toHaveBeenCalledWith({ + ssoStartUrl: mockValidatedProfile.sso_start_url, + ssoAccountId: mockValidatedProfile.sso_account_id, + ssoRegion: mockValidatedProfile.sso_region, + ssoRoleName: mockValidatedProfile.sso_role_name, + }); + }); + + it("calls fromSSO when validation succeeds", async () => { + const mockProfile = getMockOriginalSsoProfile(); + const mockValidatedProfile = getMockValidatedSsoProfile(); + + const mockCreds: Credentials = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + + (validateSsoProfile as jest.Mock).mockReturnValue(mockValidatedProfile); + (fromSSO as jest.Mock).mockReturnValue(() => Promise.resolve(mockCreds)); + + const receivedCreds = await resolveSsoCredentials(mockProfile); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(validateSsoProfile).toHaveBeenCalledWith(mockProfile); + expect(fromSSO).toHaveBeenCalledWith({ + ssoStartUrl: mockValidatedProfile.sso_start_url, + ssoAccountId: mockValidatedProfile.sso_account_id, + ssoRegion: mockValidatedProfile.sso_region, + ssoRoleName: mockValidatedProfile.sso_role_name, + }); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveSsoCredentials.ts b/packages/credential-provider-ini/src/resolveSsoCredentials.ts new file mode 100644 index 000000000000..86b34730d49e --- /dev/null +++ b/packages/credential-provider-ini/src/resolveSsoCredentials.ts @@ -0,0 +1,14 @@ +import { fromSSO, validateSsoProfile } from "@aws-sdk/credential-provider-sso"; +import { SsoProfile } from "@aws-sdk/credential-provider-sso"; + +export { isSsoProfile } from "@aws-sdk/credential-provider-sso"; + +export const resolveSsoCredentials = (data: Partial) => { + const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(data); + return fromSSO({ + ssoStartUrl: sso_start_url, + ssoAccountId: sso_account_id, + ssoRegion: sso_region, + ssoRoleName: sso_role_name, + })(); +}; diff --git a/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts b/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts new file mode 100644 index 000000000000..ab2a16773152 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts @@ -0,0 +1,49 @@ +import { isStaticCredsProfile, resolveStaticCredentials } from "./resolveStaticCredentials"; + +const getMockStaticCredsProfile = () => ({ + aws_access_key_id: "mock_aws_access_key_id", + aws_secret_access_key: "mock_aws_secret_access_key", + aws_session_token: "mock_aws_session_token", +}); + +describe(isStaticCredsProfile.name, () => { + describe("returns false for falsy values", () => { + it.each([false, 0, -0, "", null, undefined, NaN])("%s:", (falsyValue) => { + expect(isStaticCredsProfile(falsyValue)).toEqual(false); + }); + }); + + describe("returns false for data type which is not an object", () => { + it.each([true, 1, "string"])("%s:", (notObject) => { + expect(isStaticCredsProfile(notObject)).toEqual(false); + }); + }); + + it.each(["aws_access_key_id", "aws_secret_access_key"])("value at '%s' is not of type string", (key) => { + [true, null, undefined, 1, NaN, {}].forEach((value) => { + expect(isStaticCredsProfile({ ...getMockStaticCredsProfile(), [key]: value })).toEqual(false); + }); + }); + + it.each(["aws_session_token"])("value at '%s' is not of type string | undefined", (key) => { + [true, null, 1, NaN, {}].forEach((value) => { + expect(isStaticCredsProfile({ ...getMockStaticCredsProfile(), [key]: value })).toEqual(false); + }); + }); + + it("returns true for StaticCredentialsProfile", () => { + expect(isStaticCredsProfile(getMockStaticCredsProfile())).toEqual(true); + }); +}); + +describe(resolveStaticCredentials.name, () => { + it("resolves static credentials", async () => { + const mockProfile = getMockStaticCredsProfile(); + const receivedCreds = await resolveStaticCredentials(mockProfile); + expect(receivedCreds).toStrictEqual({ + accessKeyId: mockProfile.aws_access_key_id, + secretAccessKey: mockProfile.aws_secret_access_key, + sessionToken: mockProfile.aws_session_token, + }); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveStaticCredentials.ts b/packages/credential-provider-ini/src/resolveStaticCredentials.ts new file mode 100644 index 000000000000..264456c1baf2 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveStaticCredentials.ts @@ -0,0 +1,22 @@ +import { Profile } from "@aws-sdk/shared-ini-file-loader"; +import { Credentials } from "@aws-sdk/types"; + +export interface StaticCredsProfile extends Profile { + aws_access_key_id: string; + aws_secret_access_key: string; + aws_session_token?: string; +} + +export const isStaticCredsProfile = (arg: any): arg is StaticCredsProfile => + Boolean(arg) && + typeof arg === "object" && + typeof arg.aws_access_key_id === "string" && + typeof arg.aws_secret_access_key === "string" && + ["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1; + +export const resolveStaticCredentials = (profile: StaticCredsProfile): Promise => + Promise.resolve({ + accessKeyId: profile.aws_access_key_id, + secretAccessKey: profile.aws_secret_access_key, + sessionToken: profile.aws_session_token, + }); diff --git a/packages/credential-provider-ini/src/resolveWebIdentityCredentials.spec.ts b/packages/credential-provider-ini/src/resolveWebIdentityCredentials.spec.ts new file mode 100644 index 000000000000..9f955616d3b5 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveWebIdentityCredentials.spec.ts @@ -0,0 +1,94 @@ +import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity"; +import { Credentials } from "@aws-sdk/types"; + +import { isWebIdentityProfile, resolveWebIdentityCredentials } from "./resolveWebIdentityCredentials"; + +jest.mock("@aws-sdk/credential-provider-web-identity"); + +const getMockWebIdentityProfile = () => ({ + web_identity_token_file: "mock_web_identity_token_file", + role_arn: "mock_role_arn", + role_session_name: "mock_role_session_name", +}); + +describe(isWebIdentityProfile.name, () => { + describe("returns false for falsy values", () => { + it.each([false, 0, -0, "", null, undefined, NaN])("%s:", (falsyValue) => { + expect(isWebIdentityProfile(falsyValue)).toEqual(false); + }); + }); + + describe("returns false for data type which is not an object", () => { + it.each([true, 1, "string"])("%s:", (notObject) => { + expect(isWebIdentityProfile(notObject)).toEqual(false); + }); + }); + + it.each(["web_identity_token_file", "role_arn"])("value at '%s' is not of type string", (key) => { + [true, null, undefined, 1, NaN, {}].forEach((value) => { + expect(isWebIdentityProfile({ ...getMockWebIdentityProfile(), [key]: value })).toEqual(false); + }); + }); + + it.each(["role_session_name"])("value at '%s' is not of type string | undefined", (key) => { + [true, null, 1, NaN, {}].forEach((value) => { + expect(isWebIdentityProfile({ ...getMockWebIdentityProfile(), [key]: value })).toEqual(false); + }); + }); + + it("returns true for WebIdentityProfile", () => { + expect(isWebIdentityProfile(getMockWebIdentityProfile())).toEqual(true); + }); +}); + +describe(resolveWebIdentityCredentials.name, () => { + const mockCreds: Credentials = { + accessKeyId: "mockAccessKeyId", + secretAccessKey: "mockSecretAccessKey", + }; + + beforeEach(() => { + (fromTokenFile as jest.Mock).mockReturnValue(() => Promise.resolve(mockCreds)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("throws error when fromTokenFile throws", async () => { + const mockProfile = getMockWebIdentityProfile(); + const mockOptions = { roleAssumerWithWebIdentity: jest.fn() }; + const expectedError = new Error("error from fromTokenFile"); + + (fromTokenFile as jest.Mock).mockReturnValue(() => Promise.reject(expectedError)); + + try { + await resolveWebIdentityCredentials(mockProfile, mockOptions); + fail(`expected ${expectedError}`); + } catch (error) { + expect(error).toStrictEqual(expectedError); + } + expect(fromTokenFile).toHaveBeenCalledWith({ + webIdentityTokenFile: mockProfile.web_identity_token_file, + roleArn: mockProfile.role_arn, + roleSessionName: mockProfile.role_session_name, + roleAssumerWithWebIdentity: mockOptions.roleAssumerWithWebIdentity, + }); + }); + + it("returns creds from fromTokenFile", async () => { + const mockProfile = getMockWebIdentityProfile(); + const mockOptions = { roleAssumerWithWebIdentity: jest.fn() }; + + (fromTokenFile as jest.Mock).mockReturnValue(() => Promise.resolve(mockCreds)); + + const receivedCreds = await resolveWebIdentityCredentials(mockProfile, mockOptions); + expect(receivedCreds).toStrictEqual(mockCreds); + expect(fromTokenFile).toHaveBeenCalledWith({ + webIdentityTokenFile: mockProfile.web_identity_token_file, + roleArn: mockProfile.role_arn, + roleSessionName: mockProfile.role_session_name, + roleAssumerWithWebIdentity: mockOptions.roleAssumerWithWebIdentity, + }); + }); +}); diff --git a/packages/credential-provider-ini/src/resolveWebIdentityCredentials.ts b/packages/credential-provider-ini/src/resolveWebIdentityCredentials.ts new file mode 100644 index 000000000000..d0fc548de6f7 --- /dev/null +++ b/packages/credential-provider-ini/src/resolveWebIdentityCredentials.ts @@ -0,0 +1,29 @@ +import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity"; +import { Profile } from "@aws-sdk/shared-ini-file-loader"; +import { Credentials } from "@aws-sdk/types"; + +import { FromIniInit } from "./fromIni"; + +export interface WebIdentityProfile extends Profile { + web_identity_token_file: string; + role_arn: string; + role_session_name?: string; +} + +export const isWebIdentityProfile = (arg: any): arg is WebIdentityProfile => + Boolean(arg) && + typeof arg === "object" && + typeof arg.web_identity_token_file === "string" && + typeof arg.role_arn === "string" && + ["undefined", "string"].indexOf(typeof arg.role_session_name) > -1; + +export const resolveWebIdentityCredentials = async ( + profile: WebIdentityProfile, + options: FromIniInit +): Promise => + fromTokenFile({ + webIdentityTokenFile: profile.web_identity_token_file, + roleArn: profile.role_arn, + roleSessionName: profile.role_session_name, + roleAssumerWithWebIdentity: options.roleAssumerWithWebIdentity, + })();