From 49bbbeb420f5e0551d7b28d21654567348a9d088 Mon Sep 17 00:00:00 2001 From: Tom Keller Date: Tue, 18 Oct 2022 17:58:04 -0700 Subject: [PATCH] chore: add remaining tests --- .gitignore | 2 + .projen/deps.json | 4 + .projenrc.js | 3 + package-lock.json | 165 +--------- package.json | 2 +- src/assumeRole.ts | 36 +-- src/cleanup/index.ts | 1 + src/index.ts | 35 +- test/cleanup.test.ts | 8 +- test/index.test.ts | 747 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.dev.json | 2 +- tsconfig.json | 2 +- 12 files changed, 798 insertions(+), 209 deletions(-) create mode 100644 test/index.test.ts diff --git a/.gitignore b/.gitignore index b9fbb65..f5e5b14 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ jspm_packages/ *.tgz .yarn-integrity .cache +.vscode +.env !/.projenrc.js /test-reports/ junit.xml diff --git a/.projen/deps.json b/.projen/deps.json index 1010e92..831d07d 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -4,6 +4,10 @@ "name": "@aws-sdk/credential-provider-env", "type": "build" }, + { + "name": "@aws-sdk/property-provider", + "type": "build" + }, { "name": "@jest/globals", "type": "build" diff --git a/.projenrc.js b/.projenrc.js index 181603a..6515825 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -10,6 +10,7 @@ const project = new GitHubActionTypeScriptProject({ '@aws-sdk/credential-provider-env', 'aws-sdk-client-mock', '@jest/globals', + '@aws-sdk/property-provider', ], deps: ['@aws-sdk/client-sts@^3'], name: 'configure-aws-credentials', @@ -21,6 +22,7 @@ const project = new GitHubActionTypeScriptProject({ authorUrl: 'https://aws.amazon.com', packageManager: NodePackageManager.NPM, sampleCode: false, + gitignore: ['.vscode', '.env'], actionMetadata: { name: '"Configure AWS Credentials" Action for GitHub Actions', description: 'Configures AWS credentials for use in subsequent steps in a GitHub Action workflow', @@ -135,6 +137,7 @@ const project = new GitHubActionTypeScriptProject({ target: 'es2022', module: 'commonjs', outDir: 'build', + noUnusedLocals: false, }, }, prettier: true, diff --git a/package-lock.json b/package-lock.json index 335fe9e..81d208b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@aws-sdk/credential-provider-env": "^3.186.0", - "@aws-sdk/credential-providers": "^3.188.0", + "@aws-sdk/property-provider": "^3.188.0", "@jest/globals": "^29.1.2", "@types/jest": "^29.1.2", "@types/node": "^14", @@ -179,51 +179,6 @@ "node": ">= 12.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.188.0.tgz", - "integrity": "sha512-lpl8yxAjER3xUDYQxJR8oyQzfcw4TQbsgTY+kZzVTIsHVLIuZfJoWBNA/ONWkmuCOylH8jEu5jcZ3a45fyx5fg==", - "dev": true, - "dependencies": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/client-sts": "3.188.0", - "@aws-sdk/config-resolver": "3.188.0", - "@aws-sdk/credential-provider-node": "3.188.0", - "@aws-sdk/fetch-http-handler": "3.188.0", - "@aws-sdk/hash-node": "3.188.0", - "@aws-sdk/invalid-dependency": "3.188.0", - "@aws-sdk/middleware-content-length": "3.188.0", - "@aws-sdk/middleware-host-header": "3.188.0", - "@aws-sdk/middleware-logger": "3.188.0", - "@aws-sdk/middleware-recursion-detection": "3.188.0", - "@aws-sdk/middleware-retry": "3.188.0", - "@aws-sdk/middleware-serde": "3.188.0", - "@aws-sdk/middleware-signing": "3.188.0", - "@aws-sdk/middleware-stack": "3.188.0", - "@aws-sdk/middleware-user-agent": "3.188.0", - "@aws-sdk/node-config-provider": "3.188.0", - "@aws-sdk/node-http-handler": "3.188.0", - "@aws-sdk/protocol-http": "3.188.0", - "@aws-sdk/smithy-client": "3.188.0", - "@aws-sdk/types": "3.188.0", - "@aws-sdk/url-parser": "3.188.0", - "@aws-sdk/util-base64-browser": "3.188.0", - "@aws-sdk/util-base64-node": "3.188.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.188.0", - "@aws-sdk/util-defaults-mode-browser": "3.188.0", - "@aws-sdk/util-defaults-mode-node": "3.188.0", - "@aws-sdk/util-user-agent-browser": "3.188.0", - "@aws-sdk/util-user-agent-node": "3.188.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.188.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@aws-sdk/client-sso": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.188.0.tgz", @@ -325,21 +280,6 @@ "node": ">= 12.0.0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.188.0.tgz", - "integrity": "sha512-PK+a5wiQT/xz3CVVXulkYBdvejrHSmQ/JI38jW0GPQaa6zWobk1kkNOLUTqcpAaiYsyZUm+3guDUSobdHWnJ2A==", - "dev": true, - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.188.0", - "@aws-sdk/property-provider": "3.188.0", - "@aws-sdk/types": "3.188.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.188.0.tgz", @@ -448,32 +388,6 @@ "node": ">= 12.0.0" } }, - "node_modules/@aws-sdk/credential-providers": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.188.0.tgz", - "integrity": "sha512-RNf0nolOqPKGUzq2wZHo0qYz14r9W+liX5BeUK9+fqhZtYY1LOoL+Jb3SFCIMPSnYekmMfEZ7JSU00QgMrBsmA==", - "dev": true, - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.188.0", - "@aws-sdk/client-sso": "3.188.0", - "@aws-sdk/client-sts": "3.188.0", - "@aws-sdk/credential-provider-cognito-identity": "3.188.0", - "@aws-sdk/credential-provider-env": "3.188.0", - "@aws-sdk/credential-provider-imds": "3.188.0", - "@aws-sdk/credential-provider-ini": "3.188.0", - "@aws-sdk/credential-provider-node": "3.188.0", - "@aws-sdk/credential-provider-process": "3.188.0", - "@aws-sdk/credential-provider-sso": "3.188.0", - "@aws-sdk/credential-provider-web-identity": "3.188.0", - "@aws-sdk/property-provider": "3.188.0", - "@aws-sdk/shared-ini-file-loader": "3.188.0", - "@aws-sdk/types": "3.188.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/@aws-sdk/fetch-http-handler": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.188.0.tgz", @@ -8909,48 +8823,6 @@ "tslib": "^2.3.1" } }, - "@aws-sdk/client-cognito-identity": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.188.0.tgz", - "integrity": "sha512-lpl8yxAjER3xUDYQxJR8oyQzfcw4TQbsgTY+kZzVTIsHVLIuZfJoWBNA/ONWkmuCOylH8jEu5jcZ3a45fyx5fg==", - "dev": true, - "requires": { - "@aws-crypto/sha256-browser": "2.0.0", - "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/client-sts": "3.188.0", - "@aws-sdk/config-resolver": "3.188.0", - "@aws-sdk/credential-provider-node": "3.188.0", - "@aws-sdk/fetch-http-handler": "3.188.0", - "@aws-sdk/hash-node": "3.188.0", - "@aws-sdk/invalid-dependency": "3.188.0", - "@aws-sdk/middleware-content-length": "3.188.0", - "@aws-sdk/middleware-host-header": "3.188.0", - "@aws-sdk/middleware-logger": "3.188.0", - "@aws-sdk/middleware-recursion-detection": "3.188.0", - "@aws-sdk/middleware-retry": "3.188.0", - "@aws-sdk/middleware-serde": "3.188.0", - "@aws-sdk/middleware-signing": "3.188.0", - "@aws-sdk/middleware-stack": "3.188.0", - "@aws-sdk/middleware-user-agent": "3.188.0", - "@aws-sdk/node-config-provider": "3.188.0", - "@aws-sdk/node-http-handler": "3.188.0", - "@aws-sdk/protocol-http": "3.188.0", - "@aws-sdk/smithy-client": "3.188.0", - "@aws-sdk/types": "3.188.0", - "@aws-sdk/url-parser": "3.188.0", - "@aws-sdk/util-base64-browser": "3.188.0", - "@aws-sdk/util-base64-node": "3.188.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.188.0", - "@aws-sdk/util-defaults-mode-browser": "3.188.0", - "@aws-sdk/util-defaults-mode-node": "3.188.0", - "@aws-sdk/util-user-agent-browser": "3.188.0", - "@aws-sdk/util-user-agent-node": "3.188.0", - "@aws-sdk/util-utf8-browser": "3.188.0", - "@aws-sdk/util-utf8-node": "3.188.0", - "tslib": "^2.3.1" - } - }, "@aws-sdk/client-sso": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.188.0.tgz", @@ -9043,18 +8915,6 @@ "tslib": "^2.3.1" } }, - "@aws-sdk/credential-provider-cognito-identity": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.188.0.tgz", - "integrity": "sha512-PK+a5wiQT/xz3CVVXulkYBdvejrHSmQ/JI38jW0GPQaa6zWobk1kkNOLUTqcpAaiYsyZUm+3guDUSobdHWnJ2A==", - "dev": true, - "requires": { - "@aws-sdk/client-cognito-identity": "3.188.0", - "@aws-sdk/property-provider": "3.188.0", - "@aws-sdk/types": "3.188.0", - "tslib": "^2.3.1" - } - }, "@aws-sdk/credential-provider-env": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.188.0.tgz", @@ -9142,29 +9002,6 @@ "tslib": "^2.3.1" } }, - "@aws-sdk/credential-providers": { - "version": "3.188.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.188.0.tgz", - "integrity": "sha512-RNf0nolOqPKGUzq2wZHo0qYz14r9W+liX5BeUK9+fqhZtYY1LOoL+Jb3SFCIMPSnYekmMfEZ7JSU00QgMrBsmA==", - "dev": true, - "requires": { - "@aws-sdk/client-cognito-identity": "3.188.0", - "@aws-sdk/client-sso": "3.188.0", - "@aws-sdk/client-sts": "3.188.0", - "@aws-sdk/credential-provider-cognito-identity": "3.188.0", - "@aws-sdk/credential-provider-env": "3.188.0", - "@aws-sdk/credential-provider-imds": "3.188.0", - "@aws-sdk/credential-provider-ini": "3.188.0", - "@aws-sdk/credential-provider-node": "3.188.0", - "@aws-sdk/credential-provider-process": "3.188.0", - "@aws-sdk/credential-provider-sso": "3.188.0", - "@aws-sdk/credential-provider-web-identity": "3.188.0", - "@aws-sdk/property-provider": "3.188.0", - "@aws-sdk/shared-ini-file-loader": "3.188.0", - "@aws-sdk/types": "3.188.0", - "tslib": "^2.3.1" - } - }, "@aws-sdk/fetch-http-handler": { "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.188.0.tgz", diff --git a/package.json b/package.json index c418cdd..5fde7b6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "devDependencies": { "@aws-sdk/credential-provider-env": "^3.186.0", - "@aws-sdk/credential-providers": "^3.188.0", + "@aws-sdk/property-provider": "^3.188.0", "@jest/globals": "^29.1.2", "@types/jest": "^29.1.2", "@types/node": "^14", diff --git a/src/assumeRole.ts b/src/assumeRole.ts index c1c25e3..e722c78 100644 --- a/src/assumeRole.ts +++ b/src/assumeRole.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import fs from 'fs'; import path from 'path'; import * as core from '@actions/core'; -import { AssumeRoleCommandInput, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; +import { AssumeRoleCommand, AssumeRoleCommandInput, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; import { errorMessage, getStsClient, isDefined } from './helpers'; const SANITIZATION_CHARACTER = '_'; @@ -94,15 +94,16 @@ export async function assumeRole(params: assumeRoleParams) { const keys = Object.keys(commonAssumeRoleParams) as Array; keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); - let assumeRoleCommand: AssumeRoleWithWebIdentityCommand; + const sts = getStsClient(region); switch (true) { case !!webIdentityToken: { delete commonAssumeRoleParams.Tags; - assumeRoleCommand = new AssumeRoleWithWebIdentityCommand({ - ...commonAssumeRoleParams, - WebIdentityToken: webIdentityToken, - }); - break; + return sts.send( + new AssumeRoleWithWebIdentityCommand({ + ...commonAssumeRoleParams, + WebIdentityToken: webIdentityToken, + }) + ); } case !!webIdentityTokenFile: { core.debug( @@ -117,21 +118,20 @@ export async function assumeRole(params: assumeRoleParams) { } try { - const widt = await fs.promises.readFile(webIdentityTokenFilePath, 'utf8'); + const widt = fs.readFileSync(webIdentityTokenFilePath, 'utf8'); delete commonAssumeRoleParams.Tags; - assumeRoleCommand = new AssumeRoleWithWebIdentityCommand({ - ...commonAssumeRoleParams, - WebIdentityToken: widt, - }); + return await sts.send( + new AssumeRoleWithWebIdentityCommand({ + ...commonAssumeRoleParams, + WebIdentityToken: widt, + }) + ); } catch (error) { throw new Error(`Web identity token file could not be read: ${errorMessage(error)}`); } - break; } - default: - throw new Error('No web identity token or web identity token file provided.'); + default: { + return sts.send(new AssumeRoleCommand({ ...commonAssumeRoleParams })); + } } - - const sts = getStsClient(region); - return sts.send(assumeRoleCommand); } diff --git a/src/cleanup/index.ts b/src/cleanup/index.ts index b30557b..6bf6593 100644 --- a/src/cleanup/index.ts +++ b/src/cleanup/index.ts @@ -27,6 +27,7 @@ export async function cleanup() { core.setFailed(errorMessage(error)); } } +/* istanbul ignore next */ if (require.main === module) { (async () => { await cleanup(); diff --git a/src/index.ts b/src/index.ts index 3778ce5..5760389 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,19 +51,19 @@ function exportRegion(region: string) { async function exportAccountId(region: string, maskAccountId?: boolean) { // Get the AWS account ID const client = getStsClient(region, USER_AGENT); - const identity = (await client.send(new GetCallerIdentityCommand({}))).Account; - if (!identity) { + const identity = await client.send(new GetCallerIdentityCommand({})); + const accountId = identity.Account; + if (!accountId) { throw new Error('Could not get Account ID from STS. Did you set credentials?'); } if (maskAccountId) { - core.setSecret(identity); - } else { - core.setOutput('aws-account-id', identity); + core.setSecret(accountId); } - return identity; + core.setOutput('aws-account-id', accountId); + return accountId; } -function loadCredentials() { +async function loadCredentials() { // Previously, this function forced the SDK to re-resolve credentials with the default provider chain. // // This action typically sets credentials in the environment via environment variables. The SDK never refreshed those @@ -109,25 +109,19 @@ export async function run() { (core.getInput('mask-aws-account-id', { required: false }) || 'true').toLowerCase() === 'true'; const roleToAssume = core.getInput('role-to-assume', { required: false }); const roleExternalId = core.getInput('role-external-id', { required: false }); - const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; - const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false'; - const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true'; const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false }); - // This wraps the logic for deciding if we should rely on the GH OIDC provider since we may need to reference // the decision in a few differennt places. Consolidating it here makes the logic clearer elsewhere. - const useGitHubOIDCProvider = () => { - // The assumption here is that self-hosted runners won't be populating the `ACTIONS_ID_TOKEN_REQUEST_TOKEN` - // environment variable and they won't be providing a web idenity token file or access key either. - // V2 of the action might relax this a bit and create an explicit precedence for these so that customers - // can provide as much info as they want and we will follow the established credential loading precedence. - return !!(roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !AccessKeyId && !webIdentityTokenFile); - }; + const useGitHubOIDCProvider = + !!roleToAssume && !!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !AccessKeyId && !webIdentityTokenFile; const roleDurationSeconds = parseInt(core.getInput('role-duration-seconds', { required: false })) || (SessionToken && SESSION_ROLE_DURATION) || - (useGitHubOIDCProvider() && DEFAULT_ROLE_DURATION_FOR_OIDC_ROLES) || + (useGitHubOIDCProvider && DEFAULT_ROLE_DURATION_FOR_OIDC_ROLES) || MAX_ACTION_RUNTIME; + const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; + const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false'; + const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true'; if (!region.match(REGION_REGEX)) { throw new Error(`Region is not valid: ${region}`); @@ -153,7 +147,7 @@ export async function run() { // The only way to assume the role is via GitHub's OIDC provider. let sourceAccountId: string; let webIdentityToken: string; - if (useGitHubOIDCProvider()) { + if (useGitHubOIDCProvider) { webIdentityToken = await core.getIDToken(audience); // We don't validate the credentials here because we don't have them yet when using OIDC. } else { @@ -203,6 +197,7 @@ export async function run() { } } +/* istanbul ignore next */ if (require.main === module) { (async () => { await run(); diff --git a/test/cleanup.test.ts b/test/cleanup.test.ts index 0630690..0c92f27 100644 --- a/test/cleanup.test.ts +++ b/test/cleanup.test.ts @@ -18,10 +18,10 @@ describe('Configure AWS Credentials', () => { beforeEach(() => { jest.resetModules(); - jest.spyOn(core, 'exportVariable'); - jest.spyOn(core, 'setSecret'); - jest.spyOn(core, 'setOutput'); - jest.spyOn(core, 'setFailed'); + jest.spyOn(core, 'exportVariable').mockImplementation(); + jest.spyOn(core, 'setSecret').mockImplementation(); + jest.spyOn(core, 'setOutput').mockImplementation(); + jest.spyOn(core, 'setFailed').mockImplementation(); process.env = { ...OLD_ENV, ...ACTION_ENVIRONMENT_VARIABLES }; }); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..4ca0b8b --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,747 @@ +import assert from 'assert'; +import * as core from '@actions/core'; +import { + AssumeRoleCommand, + AssumeRoleWithWebIdentityCommand, + GetCallerIdentityCommand, + STSClient, +} from '@aws-sdk/client-sts'; +import { fromEnv } from '@aws-sdk/credential-provider-env'; +import { CredentialsProviderError } from '@aws-sdk/property-provider'; +import { mockClient } from 'aws-sdk-client-mock'; +import { withsleep, reset } from '../src/helpers'; +import { run } from '../src/index'; + +// #region +const FAKE_ACCESS_KEY_ID = 'MY-AWS-ACCESS-KEY-ID'; +const FAKE_SECRET_ACCESS_KEY = 'MY-AWS-SECRET-ACCESS-KEY'; +const FAKE_SESSION_TOKEN = 'MY-AWS-SESSION-TOKEN'; +const FAKE_STS_ACCESS_KEY_ID = 'STS-AWS-ACCESS-KEY-ID'; +const FAKE_STS_SECRET_ACCESS_KEY = 'STS-AWS-SECRET-ACCESS-KEY'; +const FAKE_STS_SESSION_TOKEN = 'STS-AWS-SESSION-TOKEN'; +const FAKE_REGION = 'fake-region-1'; +const FAKE_ACCOUNT_ID = '123456789012'; +const FAKE_ROLE_ACCOUNT_ID = '111111111111'; +const ROLE_NAME = 'MY-ROLE'; +const ROLE_ARN = 'arn:aws:iam::111111111111:role/MY-ROLE'; +const ENVIRONMENT_VARIABLE_OVERRIDES = { + SHOW_STACK_TRACE: 'false', + GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME', + GITHUB_WORKFLOW: 'MY-WORKFLOW-ID', + GITHUB_ACTION: 'MY-ACTION-NAME', + GITHUB_ACTOR: 'MY-USERNAME[bot]', + GITHUB_SHA: 'MY-COMMIT-ID', + GITHUB_REF: 'MY-BRANCH', + GITHUB_WORKSPACE: '/home/github', +}; +const GITHUB_ACTOR_SANITIZED = 'MY-USERNAME_bot_'; +const CREDS_INPUTS = { + 'aws-access-key-id': FAKE_ACCESS_KEY_ID, + 'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY, +}; +const DEFAULT_INPUTS = { + ...CREDS_INPUTS, + 'aws-session-token': FAKE_SESSION_TOKEN, + 'aws-region': FAKE_REGION, + 'mask-aws-account-id': 'TRUE', +}; +const ASSUME_ROLE_INPUTS = { ...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION }; +// #endregion + +const mockedSTS = mockClient(STSClient); +function mockGetInput(requestResponse: Record) { + return function (name: string, _options: unknown) { + return requestResponse[name]; + }; +} + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + existsSync: jest.fn(() => true), + readFileSync: jest.fn(() => 'testpayload'), +})); +jest.mock('@aws-sdk/credential-provider-env', () => ({ + // This is the actual implementation in the SDK ^_^ + fromEnv: jest.fn().mockImplementation(() => async () => { + const accessKeyId = process.env.AWS_ACCESS_KEY_ID; + const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; + const sessionToken = process.env.AWS_SESSION_TOKEN; + const expiration = process.env.AWS_CREDENTIAL_EXPIRATION; + return { + accessKeyId, + secretAccessKey, + sessionToken, + expiration, + }; + }), +})); + +describe('Configure AWS Credentials', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV, ...ENVIRONMENT_VARIABLE_OVERRIDES }; + jest.clearAllMocks(); + mockedSTS.reset(); + (fromEnv as jest.Mock).mockReset(); + jest.spyOn(core, 'getIDToken').mockImplementation(() => Promise.resolve('testtoken')); + jest.spyOn(core, 'exportVariable').mockImplementation(); + jest.spyOn(core, 'setSecret').mockImplementation(); + jest.spyOn(core, 'setOutput').mockImplementation(); + jest.spyOn(core, 'setFailed').mockImplementation(); + jest.spyOn(core, 'debug').mockImplementation(); + (fromEnv as jest.Mock) + .mockImplementationOnce(() => async () => ({ + accessKeyId: FAKE_ACCESS_KEY_ID, + secretAccessKey: FAKE_SECRET_ACCESS_KEY, + })) + .mockImplementationOnce(() => async () => ({ + accessKeyId: FAKE_STS_ACCESS_KEY_ID, + secretAccessKey: FAKE_STS_SECRET_ACCESS_KEY, + })); + mockedSTS + .on(GetCallerIdentityCommand) + .resolvesOnce({ Account: FAKE_ACCOUNT_ID }) + .resolvesOnce({ Account: FAKE_ROLE_ACCOUNT_ID }); + mockedSTS.on(AssumeRoleCommand).resolves({ + Credentials: { + AccessKeyId: FAKE_STS_ACCESS_KEY_ID, + SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY, + SessionToken: FAKE_STS_SESSION_TOKEN, + Expiration: new Date(8640000000000000), + }, + }); + mockedSTS.on(AssumeRoleWithWebIdentityCommand).resolves({ + Credentials: { + AccessKeyId: FAKE_STS_ACCESS_KEY_ID, + SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY, + SessionToken: FAKE_STS_SESSION_TOKEN, + Expiration: new Date(8640000000000000), + }, + }); + withsleep(() => { + return Promise.resolve(); + }); + }); + + afterEach(() => { + process.env = OLD_ENV; + reset(); + }); + + test('exports env vars', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(DEFAULT_INPUTS)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(5); + expect(core.setSecret).toHaveBeenCalledTimes(4); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', FAKE_SESSION_TOKEN); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SESSION_TOKEN); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', FAKE_REGION); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', FAKE_REGION); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); + }); + + test('action fails when github env vars are not set', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + delete process.env.GITHUB_SHA; + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + 'Missing required environment variables. Are you running in GitHub Actions?' + ); + }); + + test('action does not require GITHUB_REF env var', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(DEFAULT_INPUTS)); + delete process.env.GITHUB_REF; + + await run(); + + expect(core.setFailed).toHaveBeenCalledTimes(0); + }); + + test('hosted runners can pull creds from a self-hosted environment', async () => { + const mockInputs = { 'aws-region': FAKE_REGION }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(2); + expect(core.setSecret).toHaveBeenCalledTimes(1); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', FAKE_REGION); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', FAKE_REGION); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); + }); + + test('action with no accessible credentials fails', async () => { + const mockInputs = { 'aws-region': FAKE_REGION }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + (fromEnv as jest.Mock).mockReset(); + (fromEnv as jest.Mock).mockImplementation(() => async () => { + throw new CredentialsProviderError('test'); + }); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + 'Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers' + ); + }); + + test('action with empty credentials fails', async () => { + const mockInputs = { 'aws-region': FAKE_REGION }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + (fromEnv as jest.Mock).mockReset(); + (fromEnv as jest.Mock).mockImplementation( + () => async () => Promise.resolve({ accessKeyId: '', secretAccessKey: '' }) + ); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + 'Credentials could not be loaded, please check your action inputs: Access key ID empty after loading credentials' + ); + }); + + test('action fails when credentials are not set in the SDK correctly', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(DEFAULT_INPUTS)); + (fromEnv as jest.Mock).mockReset(); + (fromEnv as jest.Mock).mockImplementationOnce(() => async () => Promise.resolve({ accessKeyId: '123' })); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + 'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action' + ); + }); + + test('session token is optional', async () => { + const mockInputs = { ...CREDS_INPUTS, 'aws-region': 'eu-west-1' }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(4); + expect(core.setSecret).toHaveBeenCalledTimes(3); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'eu-west-1'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'eu-west-1'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); + }); + + test('existing env var creds are cleared', async () => { + const mockInputs = { ...CREDS_INPUTS, 'aws-region': 'eu-west-1' }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + process.env.AWS_ACCESS_KEY_ID = 'foo'; + process.env.AWS_SECRET_ACCESS_KEY = 'bar'; + process.env.AWS_SESSION_TOKEN = 'helloworld'; + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(5); + expect(core.setSecret).toHaveBeenCalledTimes(3); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', ''); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'eu-west-1'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'eu-west-1'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); + }); + + test('validates region name', async () => { + const mockInputs = { ...CREDS_INPUTS, 'aws-region': '$AWS_REGION' }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith('Region is not valid: $AWS_REGION'); + }); + + test('throws error if access key id exists but missing secret access key', async () => { + const inputsWIthoutSecretKey = { ...DEFAULT_INPUTS }; + //@ts-expect-error deleting a required property to test failure condition + delete inputsWIthoutSecretKey['aws-secret-access-key']; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(inputsWIthoutSecretKey)); + + await run(); + + expect(core.setFailed).toHaveBeenCalledWith( + "'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided" + ); + }); + + test('can opt out of masking account ID', async () => { + const mockInputs = { ...CREDS_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false' }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(4); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'us-east-1'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'us-east-1'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledTimes(2); + }); + + test('error is caught by core.setFailed and caught', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(DEFAULT_INPUTS)); + mockedSTS.reset(); + mockedSTS.on(GetCallerIdentityCommand).rejects(); + + await run(); + + expect(core.setFailed).toHaveBeenCalled(); + }); + + test('basic role assumption exports', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(1); + expect(core.exportVariable).toHaveBeenCalledTimes(7); + expect(core.setSecret).toHaveBeenCalledTimes(7); + expect(core.setOutput).toHaveBeenCalledTimes(2); + + // first the source credentials are exported and masked + expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_ACCOUNT_ID); + + expect(core.exportVariable).toHaveBeenNthCalledWith(1, 'AWS_DEFAULT_REGION', FAKE_REGION); + expect(core.exportVariable).toHaveBeenNthCalledWith(2, 'AWS_REGION', FAKE_REGION); + expect(core.exportVariable).toHaveBeenNthCalledWith(3, 'AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenNthCalledWith(4, 'AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + + expect(core.setOutput).toHaveBeenNthCalledWith(1, 'aws-account-id', FAKE_ACCOUNT_ID); + + // then the role credentials are exported and masked + expect(core.setSecret).toHaveBeenNthCalledWith(4, FAKE_STS_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenNthCalledWith(5, FAKE_STS_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenNthCalledWith(6, FAKE_STS_SESSION_TOKEN); + expect(core.setSecret).toHaveBeenNthCalledWith(7, FAKE_ROLE_ACCOUNT_ID); + + expect(core.exportVariable).toHaveBeenNthCalledWith(5, 'AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenNthCalledWith(6, 'AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenNthCalledWith(7, 'AWS_SESSION_TOKEN', FAKE_STS_SESSION_TOKEN); + + expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID); + }); + + test('assume role can pull source credentials from self-hosted environment', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(1); + expect(core.exportVariable).toHaveBeenCalledTimes(5); + expect(core.setSecret).toHaveBeenCalledTimes(5); + expect(core.setOutput).toHaveBeenCalledTimes(2); + + // first the source account is exported and masked + expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCOUNT_ID); + expect(core.exportVariable).toHaveBeenNthCalledWith(1, 'AWS_DEFAULT_REGION', FAKE_REGION); + expect(core.exportVariable).toHaveBeenNthCalledWith(2, 'AWS_REGION', FAKE_REGION); + expect(core.setOutput).toHaveBeenNthCalledWith(1, 'aws-account-id', FAKE_ACCOUNT_ID); + + // then the role credentials are exported and masked + expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenNthCalledWith(4, FAKE_STS_SESSION_TOKEN); + expect(core.setSecret).toHaveBeenNthCalledWith(5, FAKE_ROLE_ACCOUNT_ID); + + expect(core.exportVariable).toHaveBeenNthCalledWith(3, 'AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenNthCalledWith(4, 'AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenNthCalledWith(5, 'AWS_SESSION_TOKEN', FAKE_STS_SESSION_TOKEN); + + expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID); + }); + + test('role assumption tags', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('role assumption duration provided', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'role-duration-seconds': '5' })); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 5, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('role assumption session name provided', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'role-session-name': 'MySessionName' })); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'MySessionName', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('sets durationSeconds to one hour when session token provided and no duration is provided', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'aws-session-token': FAKE_SESSION_TOKEN })); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('sets durationSeconds to one 6 hours no session token or duration is provided', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS })); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('role name provided instead of ARN', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...CREDS_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION })); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: 'arn:aws:iam::123456789012:role/MY-ROLE', + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('web identity token file provided with absolute path', async () => { + jest.spyOn(core, 'getInput').mockImplementation( + mockGetInput({ + 'role-to-assume': ROLE_ARN, + 'aws-region': FAKE_REGION, + 'web-identity-token-file': '/fake/token/file', + }) + ); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input).toEqual({ + RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE', + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + WebIdentityToken: 'testpayload', + }); + }); + + test('web identity token file provided with relative path', async () => { + jest.spyOn(core, 'getInput').mockImplementation( + mockGetInput({ + 'role-to-assume': ROLE_ARN, + 'aws-region': FAKE_REGION, + 'web-identity-token-file': 'fake/token/file', + }) + ); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input).toEqual({ + RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE', + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + WebIdentityToken: 'testpayload', + }); + }); + + test('only role arn and region provided to use GH OIDC Token', async () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token'; + + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input).toEqual({ + RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE', + RoleSessionName: 'GitHubActions', + DurationSeconds: 3600, + WebIdentityToken: 'testtoken', + }); + expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_STS_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN); + }); + + test('GH OIDC With custom role duration', async () => { + const CUSTOM_ROLE_DURATION = '1234'; + process.env.GITHUB_ACTIONS = 'true'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token'; + jest.spyOn(core, 'getInput').mockImplementation( + mockGetInput({ + 'role-to-assume': ROLE_ARN, + 'aws-region': FAKE_REGION, + 'role-duration-seconds': CUSTOM_ROLE_DURATION, + }) + ); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input).toEqual({ + RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE', + RoleSessionName: 'GitHubActions', + DurationSeconds: parseInt(CUSTOM_ROLE_DURATION), + WebIdentityToken: 'testtoken', + }); + expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_STS_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN); + }); + + test('role assumption fails after maximum trials using OIDC provider', async () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token'; + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION })); + + mockedSTS.reset(); + mockedSTS.on(AssumeRoleWithWebIdentityCommand).rejects(); + + await run(); + expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(12); + }); + + test('role external ID provided', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'role-external-id': 'abcdef' })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + ExternalId: 'abcdef', + }); + }); + + test('workflow name sanitized in role assumption tags', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + process.env = { + ...process.env, + GITHUB_WORKFLOW: + 'Workflow!"#$%&\'()*+, -./:;<=>?@[]^_`{|}~🙂💥🍌1yFvMOeD3ZHYsHrGjCceOboMYzBPo0CRNFdcsVRG6UgR3A912a8KfcBtEVvkAS7kRBq80umGff8mux5IN1y55HQWPNBNyaruuVr4islFXte4FDQZexGJRUSMyHQpxJ8OmZnET84oDmbvmIjgxI6IBrdihX9PHMapT4gQvRYnLqNiKb18rEMWDNoZRy51UPX5sWK2GKPipgKSO9kqLckZai9D2AN2RlWCxtMqChNtxuxjqeqhoQZo0oaq39sjcRZgAAAAAAA', + }; + + const sanitizedWorkflowName = + 'Workflow__________+_ -./:;<=>?@____________1yFvMOeD3ZHYsHrGjCceOboMYzBPo0CRNFdcsVRG6UgR3A912a8KfcBtEVvkAS7kRBq80umGff8mux5IN1y55HQWPNBNyaruuVr4islFXte4FDQZexGJRUSMyHQpxJ8OmZnET84oDmbvmIjgxI6IBrdihX9PHMapT4gQvRYnLqNiKb18rEMWDNoZRy51UPX5sWK2GKPipgKSO9kqLckZa'; + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 6 * 3600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: sanitizedWorkflowName }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('skip tagging provided as true', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'role-skip-session-tagging': 'true' })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 21600, + Tags: undefined, + }); + }); + + test('skip tagging provided as false', async () => { + jest + .spyOn(core, 'getInput') + .mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'role-skip-session-tagging': 'false' })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 21600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('skip tagging not provided', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS })); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)[0].args[0].input).toEqual({ + RoleArn: ROLE_ARN, + RoleSessionName: 'GitHubActions', + DurationSeconds: 21600, + Tags: [ + { Key: 'GitHub', Value: 'Actions' }, + { Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY }, + { Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW }, + { Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION }, + { Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED }, + { Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA }, + { Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF }, + ], + }); + }); + + test('masks variables before exporting', async () => { + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + const maskedValues: string[] = []; + const publicFields = ['AWS_REGION', 'AWS_DEFAULT_REGION']; + jest.spyOn(core, 'setSecret').mockImplementation((secret) => { + maskedValues.push(secret); + }); + jest.spyOn(core, 'exportVariable').mockImplementation((name, value) => { + if (!maskedValues.includes(value) && !publicFields.includes(name)) { + throw new Error(value + ' for variable ' + name + ' is not masked yet!'); + } + process.env[name] = value; + }); + + await run(); + + expect(core.exportVariable).toReturn(); + }); +}); diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 6760eb0..b578f50 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -15,7 +15,7 @@ "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": true, "resolveJsonModule": true, "strict": true, diff --git a/tsconfig.json b/tsconfig.json index 637251b..d568ac6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": true, "resolveJsonModule": true, "strict": true,