Skip to content

Commit

Permalink
feat: add support for assuming a role (#17)
Browse files Browse the repository at this point in the history
* first draft attempt at adding role assumption option

* refinements

* const not var

* clean up asserts

* set explicit sts endpoint and clarify required inputs error message

* streamline mocks

* add new inputs to Action definition

* ignore .idea directory

* add initial assume role test

* make tests fail usefully when not in GitHub Actions

* add logic to handle suppression of stack trace

* pull credentials exports out into function

* convert environment variable patching to use object for source and add needed members

* add test for STS call

* compartmentalization and use custom user agent in role assumption STS client

* change DO_NOT_SUPRESS_STACK_TRACE to SHOW_STACK_TRACE

* update role-to-assume input description
  • Loading branch information
mattsb42-aws authored and clareliguori committed Jan 22, 2020
1 parent e3c83cf commit 25960ab
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 59 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/

# Editors
.vscode
.idea

# Logs
logs
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)"
required: false
outputs:
aws-account-id:
description: 'The AWS account ID for the provided credentials'
Expand Down
138 changes: 109 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
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.
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.
const isDefined = i => !!i;

const {roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, sessionToken, region} = params;
assert(
[roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region].every(isDefined),
"Missing required input when assuming a Role."
);

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?'
);

const endpoint = util.format('https://sts.%s.amazonaws.com', region);

const sts = new aws.STS({
accessKeyId, secretAccessKey, sessionToken, region, endpoint, customUserAgent: USER_AGENT
});
return sts.assumeRole({
RoleArn: roleToAssume,
RoleSessionName: 'GitHubActions',
DurationSeconds: roleDurationSeconds,
Tags: [
{Key: 'GitHub', Value: 'Actions'},
{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},
]
})
.promise()
.then(function (data) {
return {
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.SessionToken,
};
});
}

function exportCredentials(params){
// Configure the AWS CLI and AWS SDKs using environment variables
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);
}
}

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 {
Expand All @@ -9,41 +98,32 @@ async function run() {
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;

// 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);
// Get role credentials if configured to do so
if (roleToAssume) {
const roleCredentials = await assumeRole(
{accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds}
);
exportCredentials(roleCredentials);
} else {
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
}

// 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);

const showStackTrace = process.env.SHOW_STACK_TRACE;

if (showStackTrace === 'true') {
throw(error)
}

}
}

Expand Down
Loading

0 comments on commit 25960ab

Please sign in to comment.