From 231a2c9bf9d25af4c2e4031e713e81908ca79beb Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 7 Jan 2020 12:13:53 -0800 Subject: [PATCH 01/17] first draft attempt at adding role assumption option --- index.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/index.js b/index.js index fb3a1a464..9d02fe2bf 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,55 @@ const core = require('@actions/core'); const aws = require('aws-sdk'); +async function assumeRole(params) { + const sts = new aws.STS({ + accessKeyId: params.accessKeyId, + secretAccessKey:params.secretAccessKey, + sessionToken: params.sessionToken, + region: params.region}); + + sts.assumeRole({ + RoleArn: params.roleToAssume, + RoleSessionName: 'GitHub Actions', + DurationSeconds: params.roleDurationSeconds, + Tags: [ + {Key: 'GitHub', Value: 'Actions'}, + {Key: 'Repository', Value: process.env.GITHUB_REPOSITORY}, + {Key: 'Workflow', Value: process.env.GITHUB_WORKFLOW}, + {Key: 'Action', Value: process.env.GITHUB_ACTION}, + {Key: 'Actor', Value: process.env.GITHUB_ACTOR}, + {Key: 'Branch', Value: process.env.GITHUB_REF}, + {Key: 'Commit', Value: process.env.GITHUB_SHA}, + ] + }, function f(err, data) { + if (err) console.log(err, err.stack); + else return { + accessKeyId: data.Credentials.AccessKeyId, + secretAccessKey: data.Credentials.SecretAccessKey, + sessionToken: data.Credentials.SessionToken, + }; + }); +} + async function run() { try { // Get inputs + const MAX_ACTION_RUNTIME = 6 * 3600; const accessKeyId = core.getInput('aws-access-key-id', { required: true }); const secretAccessKey = core.getInput('aws-secret-access-key', { required: true }); const region = core.getInput('aws-region', { required: true }); const sessionToken = core.getInput('aws-session-token', { required: false }); const maskAccountId = core.getInput('mask-aws-account-id', { required: false }); + const roleToAssume = core.getInput('role-to-assume', {required: false}); + const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME; + + + // Get role credentials if configured to do so + if (roleToAssume) { + const {accessKeyId, secretAccessKey, sessionToken} = assumeRole( + {accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds} + ); + } // Configure the AWS CLI and AWS SDKs using environment variables From 9300b8d4cc165e050f5c47dddf0a3c9e3e2f4b19 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 7 Jan 2020 13:07:41 -0800 Subject: [PATCH 02/17] refinements --- index.js | 55 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index 9d02fe2bf..7dc126ac1 100644 --- a/index.js +++ b/index.js @@ -1,40 +1,46 @@ const core = require('@actions/core'); const aws = require('aws-sdk'); +const assert = require('assert'); + +const MAX_ACTION_RUNTIME = 6 * 3600; async function assumeRole(params) { - const sts = new aws.STS({ - accessKeyId: params.accessKeyId, - secretAccessKey:params.secretAccessKey, - sessionToken: params.sessionToken, - region: params.region}); + const {roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, sessionToken, region} = params; + + const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region}); + const {GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA} = process.env; + + for (var required in [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region, GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA]) { + assert(required, 'Missing required value. Are you running in GitHub Actions?'); + } sts.assumeRole({ - RoleArn: params.roleToAssume, - RoleSessionName: 'GitHub Actions', - DurationSeconds: params.roleDurationSeconds, + RoleArn: roleToAssume, + RoleSessionName: 'GitHubActions', + DurationSeconds: roleDurationSeconds, Tags: [ {Key: 'GitHub', Value: 'Actions'}, - {Key: 'Repository', Value: process.env.GITHUB_REPOSITORY}, - {Key: 'Workflow', Value: process.env.GITHUB_WORKFLOW}, - {Key: 'Action', Value: process.env.GITHUB_ACTION}, - {Key: 'Actor', Value: process.env.GITHUB_ACTOR}, - {Key: 'Branch', Value: process.env.GITHUB_REF}, - {Key: 'Commit', Value: process.env.GITHUB_SHA}, + {Key: 'Repository', Value: GITHUB_REPOSITORY}, + {Key: 'Workflow', Value: GITHUB_WORKFLOW}, + {Key: 'Action', Value: GITHUB_ACTION}, + {Key: 'Actor', Value: GITHUB_ACTOR}, + {Key: 'Branch', Value: GITHUB_REF}, + {Key: 'Commit', Value: GITHUB_SHA}, ] - }, function f(err, data) { - if (err) console.log(err, err.stack); - else return { - accessKeyId: data.Credentials.AccessKeyId, - secretAccessKey: data.Credentials.SecretAccessKey, - sessionToken: data.Credentials.SessionToken, - }; - }); + }) + .promise() + .then(function (data) { + return { + accessKeyId: data.Credentials.AccessKeyId, + secretAccessKey: data.Credentials.SecretAccessKey, + sessionToken: data.Credentials.SessionToken, + }; + }); } async function run() { try { // Get inputs - const MAX_ACTION_RUNTIME = 6 * 3600; const accessKeyId = core.getInput('aws-access-key-id', { required: true }); const secretAccessKey = core.getInput('aws-secret-access-key', { required: true }); const region = core.getInput('aws-region', { required: true }); @@ -43,10 +49,9 @@ async function run() { const roleToAssume = core.getInput('role-to-assume', {required: false}); const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME; - // Get role credentials if configured to do so if (roleToAssume) { - const {accessKeyId, secretAccessKey, sessionToken} = assumeRole( + const {accessKeyId, secretAccessKey, sessionToken} = await assumeRole( {accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds} ); } From 07bd0f1cd10a4f3f209799ba3cdf71f23176a4c3 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 7 Jan 2020 13:08:57 -0800 Subject: [PATCH 03/17] const not var --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7dc126ac1..95165e9aa 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ async function assumeRole(params) { const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region}); const {GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA} = process.env; - for (var required in [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region, GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA]) { + for (const required in [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region, GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA]) { assert(required, 'Missing required value. Are you running in GitHub Actions?'); } From 7e384075633d8d7de35f32e29dfc5b234e85e0eb Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 7 Jan 2020 14:38:25 -0800 Subject: [PATCH 04/17] clean up asserts --- index.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 95165e9aa..6a2becb8e 100644 --- a/index.js +++ b/index.js @@ -2,19 +2,28 @@ const core = require('@actions/core'); const aws = require('aws-sdk'); const assert = require('assert'); +// The max time that a GitHub action is allowed to run is 6 hours. +// That seems like a reasonable default to use if no role duration is defined. const MAX_ACTION_RUNTIME = 6 * 3600; async function assumeRole(params) { + // Assume a role to get short-lived credentials using longer-lived credentials. + const isDefined = i => !!i; + const {roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, sessionToken, region} = params; + assert( + [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region].every(isDefined), + "Missing required input." + ); - const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region}); const {GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA} = process.env; + assert( + [GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA].every(isDefined), + 'Missing required environment value. Are you running in GitHub Actions?' + ); - for (const required in [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region, GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA]) { - assert(required, 'Missing required value. Are you running in GitHub Actions?'); - } - - sts.assumeRole({ + const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region}); + return sts.assumeRole({ RoleArn: roleToAssume, RoleSessionName: 'GitHubActions', DurationSeconds: roleDurationSeconds, From 731510ef3c40eff36d1cbb5947607bf8a7a1fd79 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 17:27:23 -0800 Subject: [PATCH 05/17] set explicit sts endpoint and clarify required inputs error message --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 6a2becb8e..6c21499ea 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const core = require('@actions/core'); const aws = require('aws-sdk'); const assert = require('assert'); +const util = require('util'); // The max time that a GitHub action is allowed to run is 6 hours. // That seems like a reasonable default to use if no role duration is defined. @@ -13,7 +14,7 @@ async function assumeRole(params) { const {roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, sessionToken, region} = params; assert( [roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region].every(isDefined), - "Missing required input." + "Missing required input when assuming a Role." ); const {GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA} = process.env; @@ -22,7 +23,9 @@ async function assumeRole(params) { 'Missing required environment value. Are you running in GitHub Actions?' ); - const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region}); + const endpoint = util.format('https://sts.%s.amazonaws.com', region); + + const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region, endpoint}); return sts.assumeRole({ RoleArn: roleToAssume, RoleSessionName: 'GitHubActions', From 4070b331b1b69b823788b8222fd83973de953d2a Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 18:27:05 -0800 Subject: [PATCH 06/17] streamline mocks --- index.test.js | 93 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/index.test.js b/index.test.js index 1a12c6555..5740cff27 100644 --- a/index.test.js +++ b/index.test.js @@ -4,11 +4,39 @@ const run = require('.'); jest.mock('@actions/core'); +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'; + +function mockGetInput(requestResponse) { + return function (name, options) { // eslint-disable-line no-unused-vars + return requestResponse[name] + } +} +const REQUIRED_INPUTS = { + 'aws-access-key-id': FAKE_ACCESS_KEY_ID, + 'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY +}; +const DEFAULT_INPUTS = { + ...REQUIRED_INPUTS, + 'aws-session-token': FAKE_SESSION_TOKEN, + 'aws-region': FAKE_REGION, + 'mask-aws-account-id': 'TRUE' +}; + const mockStsCallerIdentity = jest.fn(); +const mockStsAssumeRole = jest.fn(); + jest.mock('aws-sdk', () => { return { STS: jest.fn(() => ({ - getCallerIdentity: mockStsCallerIdentity + getCallerIdentity: mockStsCallerIdentity, + assumeRole: mockStsAssumeRole, })) }; }); @@ -20,66 +48,72 @@ describe('Configure AWS Credentials', () => { core.getInput = jest .fn() - .mockReturnValueOnce('MY-AWS-ACCESS-KEY-ID') // aws-access-key-id - .mockReturnValueOnce('MY-AWS-SECRET-ACCESS-KEY') // aws-secret-access-key - .mockReturnValueOnce('us-east-2') // aws-default-region - .mockReturnValueOnce('MY-AWS-SESSION-TOKEN') // aws-session-token - .mockReturnValueOnce('TRUE'); // mask-aws-account-id + .mockImplementation(mockGetInput(DEFAULT_INPUTS)); mockStsCallerIdentity.mockImplementation(() => { return { promise() { - return Promise.resolve({ Account: '123456789012' }); + return Promise.resolve({ Account: FAKE_ACCOUNT_ID }); } }; }); + + mockStsAssumeRole.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ + Credentials: { + AccessKeyId: FAKE_STS_ACCESS_KEY_ID, + SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY, + SessionToken: FAKE_STS_SESSION_TOKEN + } + }); + } + } + }); }); test('exports env vars', async () => { await run(); expect(core.exportVariable).toHaveBeenCalledTimes(5); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', 'MY-AWS-ACCESS-KEY-ID'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 'MY-AWS-SECRET-ACCESS-KEY'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', 'MY-AWS-SESSION-TOKEN'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'us-east-2'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'us-east-2'); - expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', '123456789012'); - expect(core.setSecret).toHaveBeenCalledWith('123456789012'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', 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('session token is optional', async () => { + const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'eu-west-1'}; core.getInput = jest .fn() - .mockReturnValueOnce('MY-AWS-ACCESS-KEY-ID') // aws-access-key-id - .mockReturnValueOnce('MY-AWS-SECRET-ACCESS-KEY') // aws-secret-access-key - .mockReturnValueOnce('eu-west-1'); // aws-default-region + .mockImplementation(mockGetInput(mockInputs)); await run(); expect(core.exportVariable).toHaveBeenCalledTimes(4); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', 'MY-AWS-ACCESS-KEY-ID'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 'MY-AWS-SECRET-ACCESS-KEY'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 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', '123456789012'); - expect(core.setSecret).toHaveBeenCalledWith('123456789012'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); }); test('can opt out of masking account ID', async () => { + const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false'}; core.getInput = jest .fn() - .mockReturnValueOnce('MY-AWS-ACCESS-KEY-ID') // aws-access-key-id - .mockReturnValueOnce('MY-AWS-SECRET-ACCESS-KEY') // aws-secret-access-key - .mockReturnValueOnce('us-east-1') // aws-default-region - .mockReturnValueOnce('') // aws-session-token - .mockReturnValueOnce('false'); // mask-aws-account-id + .mockImplementation(mockGetInput(mockInputs)); await run(); expect(core.exportVariable).toHaveBeenCalledTimes(4); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', 'MY-AWS-ACCESS-KEY-ID'); - expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 'MY-AWS-SECRET-ACCESS-KEY'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 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', '123456789012'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); expect(core.setSecret).toHaveBeenCalledTimes(0); }); @@ -92,4 +126,5 @@ describe('Configure AWS Credentials', () => { expect(core.setFailed).toBeCalled(); }); + }); From 28e4d92f8fbe91ceadd32348ce86f1a99cd01ab8 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 18:27:26 -0800 Subject: [PATCH 07/17] add new inputs to Action definition --- action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/action.yml b/action.yml index 66df94139..7113bb8df 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,12 @@ inputs: mask-aws-account-id: description: "Whether to set the AWS account ID for these credentials as a secret value, so that it is masked in logs. Valid values are 'true' and 'false'. Defaults to true" required: false + role-to-assume: + description: "Use the provided credentials to assume a role rather than persisting the credentials directly" + required: false + role-duration-seconds: + description: "Role duration in seconds (default: 6 hours)" + required: false outputs: aws-account-id: description: 'The AWS account ID for the provided credentials' From f643a76c5a6de1ae108a8c76c3787b01e9a96300 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 18:27:49 -0800 Subject: [PATCH 08/17] ignore .idea directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 28ed554bb..77aaf7519 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ # Editors .vscode +.idea # Logs logs From 410c0dc3d61caf0e1195aaf9f9d3f7fc139c79ab Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 18:28:07 -0800 Subject: [PATCH 09/17] add initial assume role test --- index.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/index.test.js b/index.test.js index 5740cff27..326b0a195 100644 --- a/index.test.js +++ b/index.test.js @@ -12,6 +12,7 @@ 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 ROLE_NAME = 'MY-ROLE'; function mockGetInput(requestResponse) { return function (name, options) { // eslint-disable-line no-unused-vars @@ -28,6 +29,7 @@ const DEFAULT_INPUTS = { 'aws-region': FAKE_REGION, 'mask-aws-account-id': 'TRUE' }; +const ASSUME_ROLE_INPUTS = {...REQUIRED_INPUTS, 'role-to-assume': ROLE_NAME}; const mockStsCallerIdentity = jest.fn(); const mockStsAssumeRole = jest.fn(); @@ -127,4 +129,20 @@ describe('Configure AWS Credentials', () => { expect(core.setFailed).toBeCalled(); }); + test('basic role assumption', async () => { + core.getInput = jest + .fn() + .mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + await run(); + expect(core.exportVariable).toHaveBeenCalledTimes(5); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', FAKE_STS_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); + }); + }); From c8d7dfa7e50a87f2d6016271ba478681559ae0e5 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Jan 2020 20:03:32 -0800 Subject: [PATCH 10/17] make tests fail usefully when not in GitHub Actions --- index.js | 8 +++++++- index.test.js | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 6c21499ea..d8b9d06ec 100644 --- a/index.js +++ b/index.js @@ -101,7 +101,13 @@ async function run() { } } catch (error) { - core.setFailed(error.message); + const inGitHubAction = process.env.GITHUB_ACTIONS || false; + + if(inGitHubAction) { + core.setFailed(error.message); + }else{ + throw(error) + } } } diff --git a/index.test.js b/index.test.js index 326b0a195..6b5bf9f6d 100644 --- a/index.test.js +++ b/index.test.js @@ -77,6 +77,7 @@ describe('Configure AWS Credentials', () => { test('exports env vars', async () => { await run(); + expect(mockStsAssumeRole).toHaveBeenCalledTimes(0); expect(core.exportVariable).toHaveBeenCalledTimes(5); expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); @@ -94,6 +95,7 @@ describe('Configure AWS Credentials', () => { .mockImplementation(mockGetInput(mockInputs)); await run(); + expect(mockStsAssumeRole).toHaveBeenCalledTimes(0); expect(core.exportVariable).toHaveBeenCalledTimes(4); expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); @@ -110,6 +112,7 @@ describe('Configure AWS Credentials', () => { .mockImplementation(mockGetInput(mockInputs)); await run(); + expect(mockStsAssumeRole).toHaveBeenCalledTimes(0); expect(core.exportVariable).toHaveBeenCalledTimes(4); expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); @@ -135,6 +138,7 @@ describe('Configure AWS Credentials', () => { .mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); await run(); + expect(mockStsAssumeRole).toHaveBeenCalledTimes(1); expect(core.exportVariable).toHaveBeenCalledTimes(5); expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID); expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY); From 94e49638a7066d29d6ba4257ab16ca8e1453507c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 13 Jan 2020 12:14:24 -0800 Subject: [PATCH 11/17] add logic to handle suppression of stack trace --- index.js | 9 +++++---- index.test.js | 24 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index d8b9d06ec..e0b1b3e1c 100644 --- a/index.js +++ b/index.js @@ -101,13 +101,14 @@ async function run() { } } catch (error) { - const inGitHubAction = process.env.GITHUB_ACTIONS || false; + core.setFailed(error.message); - if(inGitHubAction) { - core.setFailed(error.message); - }else{ + const suppressStackTrace = process.env.DO_NOT_SUPPRESS_STACK_TRACE; + + if (suppressStackTrace === 'true') { throw(error) } + } } diff --git a/index.test.js b/index.test.js index 6b5bf9f6d..9c9ba72ef 100644 --- a/index.test.js +++ b/index.test.js @@ -1,4 +1,5 @@ const core = require('@actions/core'); +const assert = require('assert'); const run = require('.'); @@ -44,8 +45,12 @@ jest.mock('aws-sdk', () => { }); describe('Configure AWS Credentials', () => { + let originalSuppress; beforeEach(() => { + originalSuppress = process.env.DO_NOT_SUPPRESS_STACK_TRACE; + process.env.DO_NOT_SUPPRESS_STACK_TRACE = 'true'; + jest.clearAllMocks(); core.getInput = jest @@ -75,6 +80,10 @@ describe('Configure AWS Credentials', () => { }); }); + afterEach(() => { + process.env.DO_NOT_SUPPRESS_STACK_TRACE = originalSuppress; + }); + test('exports env vars', async () => { await run(); expect(mockStsAssumeRole).toHaveBeenCalledTimes(0); @@ -122,7 +131,9 @@ describe('Configure AWS Credentials', () => { expect(core.setSecret).toHaveBeenCalledTimes(0); }); - test('error is caught by core.setFailed', async () => { + test('error is caught by core.setFailed and caught', async () => { + process.env.DO_NOT_SUPPRESS_STACK_TRACE = 'false'; + mockStsCallerIdentity.mockImplementation(() => { throw new Error(); }); @@ -132,6 +143,17 @@ describe('Configure AWS Credentials', () => { expect(core.setFailed).toBeCalled(); }); + test('error is caught by core.setFailed and passed', async () => { + + mockStsCallerIdentity.mockImplementation(() => { + throw new Error(); + }); + + await assert.rejects(() => run()); + + expect(core.setFailed).toBeCalled(); + }); + test('basic role assumption', async () => { core.getInput = jest .fn() From b084b72b8fd56e80f5fb064c6175e04a6b815321 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 13 Jan 2020 13:56:28 -0800 Subject: [PATCH 12/17] pull credentials exports out into function --- index.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index e0b1b3e1c..d4d3c6565 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,24 @@ async function assumeRole(params) { }); } +function exportCredentials(params){ + const {accessKeyId, secretAccessKey, sessionToken} = params; + + // AWS_ACCESS_KEY_ID: + // Specifies an AWS access key associated with an IAM user or role + core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId); + + // AWS_SECRET_ACCESS_KEY: + // Specifies the secret key associated with the access key. This is essentially the "password" for the access key. + core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey); + + // AWS_SESSION_TOKEN: + // Specifies the session token value that is required if you are using temporary security credentials. + if (sessionToken) { + core.exportVariable('AWS_SESSION_TOKEN', sessionToken); + } +} + async function run() { try { // Get inputs @@ -63,27 +81,16 @@ async function run() { // Get role credentials if configured to do so if (roleToAssume) { - const {accessKeyId, secretAccessKey, sessionToken} = await assumeRole( + const roleCredentials = await assumeRole( {accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds} ); + exportCredentials(roleCredentials); + } else { + exportCredentials({accessKeyId, secretAccessKey, sessionToken}) } // Configure the AWS CLI and AWS SDKs using environment variables - // AWS_ACCESS_KEY_ID: - // Specifies an AWS access key associated with an IAM user or role - core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId); - - // AWS_SECRET_ACCESS_KEY: - // Specifies the secret key associated with the access key. This is essentially the "password" for the access key. - core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey); - - // AWS_SESSION_TOKEN: - // Specifies the session token value that is required if you are using temporary security credentials. - if (sessionToken) { - core.exportVariable('AWS_SESSION_TOKEN', sessionToken); - } - // AWS_DEFAULT_REGION and AWS_REGION: // Specifies the AWS Region to send requests to core.exportVariable('AWS_DEFAULT_REGION', region); From 3452b673b3c71b61921545cd4fb5941a6f7f2440 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 13 Jan 2020 13:56:56 -0800 Subject: [PATCH 13/17] convert environment variable patching to use object for source and add needed members --- index.test.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/index.test.js b/index.test.js index 9c9ba72ef..aa6ccaecd 100644 --- a/index.test.js +++ b/index.test.js @@ -14,6 +14,15 @@ const FAKE_STS_SESSION_TOKEN = 'STS-AWS-SESSION-TOKEN'; const FAKE_REGION = 'fake-region-1'; const FAKE_ACCOUNT_ID = '123456789012'; const ROLE_NAME = 'MY-ROLE'; +const ENVIRONMENT_VARIABLE_OVERRIDES = { + DO_NOT_SUPPRESS_STACK_TRACE: 'true', + GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME', + GITHUB_WORKFLOW: 'MY-WORKFLOW-ID', + GITHUB_ACTION: 'MY-ACTION-NAME', + GITHUB_ACTOR: 'MY-USERNAME', + GITHUB_REF: 'MY-BRANCH', + GITHUB_SHA: 'MY-COMMIT-ID', +}; function mockGetInput(requestResponse) { return function (name, options) { // eslint-disable-line no-unused-vars @@ -30,7 +39,7 @@ const DEFAULT_INPUTS = { 'aws-region': FAKE_REGION, 'mask-aws-account-id': 'TRUE' }; -const ASSUME_ROLE_INPUTS = {...REQUIRED_INPUTS, 'role-to-assume': ROLE_NAME}; +const ASSUME_ROLE_INPUTS = {...REQUIRED_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION}; const mockStsCallerIdentity = jest.fn(); const mockStsAssumeRole = jest.fn(); @@ -45,11 +54,13 @@ jest.mock('aws-sdk', () => { }); describe('Configure AWS Credentials', () => { - let originalSuppress; + let originalEnvironmentVariables = {}; beforeEach(() => { - originalSuppress = process.env.DO_NOT_SUPPRESS_STACK_TRACE; - process.env.DO_NOT_SUPPRESS_STACK_TRACE = 'true'; + for (const key of Object.keys(ENVIRONMENT_VARIABLE_OVERRIDES)){ + originalEnvironmentVariables[key] = process.env[key]; + process.env[key] = ENVIRONMENT_VARIABLE_OVERRIDES[key]; + } jest.clearAllMocks(); @@ -81,7 +92,9 @@ describe('Configure AWS Credentials', () => { }); afterEach(() => { - process.env.DO_NOT_SUPPRESS_STACK_TRACE = originalSuppress; + for (const key of Object.keys(originalEnvironmentVariables)){ + process.env[key] = originalEnvironmentVariables[key]; + } }); test('exports env vars', async () => { From e41ee3d9b97b9777a730cbab80fe520d67287b17 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 13 Jan 2020 16:01:00 -0800 Subject: [PATCH 14/17] add test for STS call --- index.test.js | 58 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/index.test.js b/index.test.js index aa6ccaecd..f134df5b0 100644 --- a/index.test.js +++ b/index.test.js @@ -54,13 +54,11 @@ jest.mock('aws-sdk', () => { }); describe('Configure AWS Credentials', () => { - let originalEnvironmentVariables = {}; + const OLD_ENV = process.env; beforeEach(() => { - for (const key of Object.keys(ENVIRONMENT_VARIABLE_OVERRIDES)){ - originalEnvironmentVariables[key] = process.env[key]; - process.env[key] = ENVIRONMENT_VARIABLE_OVERRIDES[key]; - } + jest.resetModules(); + process.env = {...OLD_ENV, ...ENVIRONMENT_VARIABLE_OVERRIDES}; jest.clearAllMocks(); @@ -92,9 +90,7 @@ describe('Configure AWS Credentials', () => { }); afterEach(() => { - for (const key of Object.keys(originalEnvironmentVariables)){ - process.env[key] = originalEnvironmentVariables[key]; - } + process.env = OLD_ENV; }); test('exports env vars', async () => { @@ -167,7 +163,7 @@ describe('Configure AWS Credentials', () => { expect(core.setFailed).toBeCalled(); }); - test('basic role assumption', async () => { + test('basic role assumption exports', async () => { core.getInput = jest .fn() .mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); @@ -184,4 +180,48 @@ describe('Configure AWS Credentials', () => { expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); }); + test('role assumption tags', async () => { + core.getInput = jest + .fn() + .mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); + + await run(); + expect(mockStsAssumeRole).toHaveBeenCalledWith({ + RoleArn: ROLE_NAME, + 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: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTOR}, + {Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF}, + {Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA}, + ] + }) + }); + + test('role assumption duration provided', async () => { + core.getInput = jest + .fn() + .mockImplementation(mockGetInput({...ASSUME_ROLE_INPUTS, 'role-duration-seconds': 5})); + + await run(); + expect(mockStsAssumeRole).toHaveBeenCalledWith({ + RoleArn: ROLE_NAME, + 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: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTOR}, + {Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF}, + {Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA}, + ] + }) + }); + }); From 42f8b5b56768a7f7a6df55434ee552dbfe5a6fbb Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 13 Jan 2020 16:07:14 -0800 Subject: [PATCH 15/17] compartmentalization and use custom user agent in role assumption STS client --- index.js | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index d4d3c6565..efc3dda11 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const util = require('util'); // The max time that a GitHub action is allowed to run is 6 hours. // That seems like a reasonable default to use if no role duration is defined. const MAX_ACTION_RUNTIME = 6 * 3600; +const USER_AGENT = 'configure-aws-credentials-for-github-actions'; async function assumeRole(params) { // Assume a role to get short-lived credentials using longer-lived credentials. @@ -25,7 +26,9 @@ async function assumeRole(params) { const endpoint = util.format('https://sts.%s.amazonaws.com', region); - const sts = new aws.STS({accessKeyId, secretAccessKey, sessionToken, region, endpoint}); + const sts = new aws.STS({ + accessKeyId, secretAccessKey, sessionToken, region, endpoint, customUserAgent: USER_AGENT + }); return sts.assumeRole({ RoleArn: roleToAssume, RoleSessionName: 'GitHubActions', @@ -51,6 +54,7 @@ async function assumeRole(params) { } function exportCredentials(params){ + // Configure the AWS CLI and AWS SDKs using environment variables const {accessKeyId, secretAccessKey, sessionToken} = params; // AWS_ACCESS_KEY_ID: @@ -68,6 +72,24 @@ function exportCredentials(params){ } } +function exportRegion(region) { + // AWS_DEFAULT_REGION and AWS_REGION: + // Specifies the AWS Region to send requests to + core.exportVariable('AWS_DEFAULT_REGION', region); + core.exportVariable('AWS_REGION', region); +} + +async function exportAccountId(maskAccountId) { + // Get the AWS account ID + const sts = new aws.STS({customUserAgent: USER_AGENT}); + const identity = await sts.getCallerIdentity().promise(); + const accountId = identity.Account; + core.setOutput('aws-account-id', accountId); + if (!maskAccountId || maskAccountId.toLowerCase() == 'true') { + core.setSecret(accountId); + } +} + async function run() { try { // Get inputs @@ -86,26 +108,12 @@ async function run() { ); exportCredentials(roleCredentials); } else { - exportCredentials({accessKeyId, secretAccessKey, sessionToken}) + exportCredentials({accessKeyId, secretAccessKey, sessionToken}); } - // Configure the AWS CLI and AWS SDKs using environment variables - - // AWS_DEFAULT_REGION and AWS_REGION: - // Specifies the AWS Region to send requests to - core.exportVariable('AWS_DEFAULT_REGION', region); - core.exportVariable('AWS_REGION', region); - - // Get the AWS account ID - const sts = new aws.STS({ - customUserAgent: 'configure-aws-credentials-for-github-actions' - }); - const identity = await sts.getCallerIdentity().promise(); - const accountId = identity.Account; - core.setOutput('aws-account-id', accountId); - if (!maskAccountId || maskAccountId.toLowerCase() == 'true') { - core.setSecret(accountId); - } + exportRegion(region); + + await exportAccountId(maskAccountId); } catch (error) { core.setFailed(error.message); From 9e5498a05b7d88d5ff91e06dfff95235bf1f3b93 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 21 Jan 2020 20:10:30 -0800 Subject: [PATCH 16/17] change DO_NOT_SUPRESS_STACK_TRACE to SHOW_STACK_TRACE --- index.js | 4 ++-- index.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index efc3dda11..ef4072fe7 100644 --- a/index.js +++ b/index.js @@ -118,9 +118,9 @@ async function run() { catch (error) { core.setFailed(error.message); - const suppressStackTrace = process.env.DO_NOT_SUPPRESS_STACK_TRACE; + const showStackTrace = process.env.SHOW_STACK_TRACE; - if (suppressStackTrace === 'true') { + if (showStackTrace === 'true') { throw(error) } diff --git a/index.test.js b/index.test.js index f134df5b0..cf5ca3aa1 100644 --- a/index.test.js +++ b/index.test.js @@ -15,7 +15,7 @@ const FAKE_REGION = 'fake-region-1'; const FAKE_ACCOUNT_ID = '123456789012'; const ROLE_NAME = 'MY-ROLE'; const ENVIRONMENT_VARIABLE_OVERRIDES = { - DO_NOT_SUPPRESS_STACK_TRACE: 'true', + SHOW_STACK_TRACE: 'true', GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME', GITHUB_WORKFLOW: 'MY-WORKFLOW-ID', GITHUB_ACTION: 'MY-ACTION-NAME', @@ -141,7 +141,7 @@ describe('Configure AWS Credentials', () => { }); test('error is caught by core.setFailed and caught', async () => { - process.env.DO_NOT_SUPPRESS_STACK_TRACE = 'false'; + process.env.SHOW_STACK_TRACE = 'false'; mockStsCallerIdentity.mockImplementation(() => { throw new Error(); From b2b2c025c8e34821811cf6d710b4a6b4c5116b8d Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 22 Jan 2020 10:50:49 -0800 Subject: [PATCH 17/17] update role-to-assume input description --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 7113bb8df..a3bf25ba3 100644 --- a/action.yml +++ b/action.yml @@ -20,7 +20,7 @@ inputs: description: "Whether to set the AWS account ID for these credentials as a secret value, so that it is masked in logs. Valid values are 'true' and 'false'. Defaults to true" required: false role-to-assume: - description: "Use the provided credentials to assume a role rather than persisting the credentials directly" + description: "Use the provided credentials to assume a Role and output the assumed credentials for that Role rather than the provided credentials" required: false role-duration-seconds: description: "Role duration in seconds (default: 6 hours)"