Skip to content

Commit 25960ab

Browse files
mattsb42-awsclareliguori
authored andcommitted
feat: add support for assuming a role (#17)
* 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
1 parent e3c83cf commit 25960ab

File tree

4 files changed

+278
-59
lines changed

4 files changed

+278
-59
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules/
22

33
# Editors
44
.vscode
5+
.idea
56

67
# Logs
78
logs

Diff for: action.yml

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ inputs:
1919
mask-aws-account-id:
2020
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"
2121
required: false
22+
role-to-assume:
23+
description: "Use the provided credentials to assume a Role and output the assumed credentials for that Role rather than the provided credentials"
24+
required: false
25+
role-duration-seconds:
26+
description: "Role duration in seconds (default: 6 hours)"
27+
required: false
2228
outputs:
2329
aws-account-id:
2430
description: 'The AWS account ID for the provided credentials'

Diff for: index.js

+109-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,94 @@
11
const core = require('@actions/core');
22
const aws = require('aws-sdk');
3+
const assert = require('assert');
4+
const util = require('util');
5+
6+
// The max time that a GitHub action is allowed to run is 6 hours.
7+
// That seems like a reasonable default to use if no role duration is defined.
8+
const MAX_ACTION_RUNTIME = 6 * 3600;
9+
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
10+
11+
async function assumeRole(params) {
12+
// Assume a role to get short-lived credentials using longer-lived credentials.
13+
const isDefined = i => !!i;
14+
15+
const {roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, sessionToken, region} = params;
16+
assert(
17+
[roleToAssume, roleDurationSeconds, accessKeyId, secretAccessKey, region].every(isDefined),
18+
"Missing required input when assuming a Role."
19+
);
20+
21+
const {GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA} = process.env;
22+
assert(
23+
[GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_REF, GITHUB_SHA].every(isDefined),
24+
'Missing required environment value. Are you running in GitHub Actions?'
25+
);
26+
27+
const endpoint = util.format('https://sts.%s.amazonaws.com', region);
28+
29+
const sts = new aws.STS({
30+
accessKeyId, secretAccessKey, sessionToken, region, endpoint, customUserAgent: USER_AGENT
31+
});
32+
return sts.assumeRole({
33+
RoleArn: roleToAssume,
34+
RoleSessionName: 'GitHubActions',
35+
DurationSeconds: roleDurationSeconds,
36+
Tags: [
37+
{Key: 'GitHub', Value: 'Actions'},
38+
{Key: 'Repository', Value: GITHUB_REPOSITORY},
39+
{Key: 'Workflow', Value: GITHUB_WORKFLOW},
40+
{Key: 'Action', Value: GITHUB_ACTION},
41+
{Key: 'Actor', Value: GITHUB_ACTOR},
42+
{Key: 'Branch', Value: GITHUB_REF},
43+
{Key: 'Commit', Value: GITHUB_SHA},
44+
]
45+
})
46+
.promise()
47+
.then(function (data) {
48+
return {
49+
accessKeyId: data.Credentials.AccessKeyId,
50+
secretAccessKey: data.Credentials.SecretAccessKey,
51+
sessionToken: data.Credentials.SessionToken,
52+
};
53+
});
54+
}
55+
56+
function exportCredentials(params){
57+
// Configure the AWS CLI and AWS SDKs using environment variables
58+
const {accessKeyId, secretAccessKey, sessionToken} = params;
59+
60+
// AWS_ACCESS_KEY_ID:
61+
// Specifies an AWS access key associated with an IAM user or role
62+
core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId);
63+
64+
// AWS_SECRET_ACCESS_KEY:
65+
// Specifies the secret key associated with the access key. This is essentially the "password" for the access key.
66+
core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey);
67+
68+
// AWS_SESSION_TOKEN:
69+
// Specifies the session token value that is required if you are using temporary security credentials.
70+
if (sessionToken) {
71+
core.exportVariable('AWS_SESSION_TOKEN', sessionToken);
72+
}
73+
}
74+
75+
function exportRegion(region) {
76+
// AWS_DEFAULT_REGION and AWS_REGION:
77+
// Specifies the AWS Region to send requests to
78+
core.exportVariable('AWS_DEFAULT_REGION', region);
79+
core.exportVariable('AWS_REGION', region);
80+
}
81+
82+
async function exportAccountId(maskAccountId) {
83+
// Get the AWS account ID
84+
const sts = new aws.STS({customUserAgent: USER_AGENT});
85+
const identity = await sts.getCallerIdentity().promise();
86+
const accountId = identity.Account;
87+
core.setOutput('aws-account-id', accountId);
88+
if (!maskAccountId || maskAccountId.toLowerCase() == 'true') {
89+
core.setSecret(accountId);
90+
}
91+
}
392

493
async function run() {
594
try {
@@ -9,41 +98,32 @@ async function run() {
998
const region = core.getInput('aws-region', { required: true });
1099
const sessionToken = core.getInput('aws-session-token', { required: false });
11100
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
101+
const roleToAssume = core.getInput('role-to-assume', {required: false});
102+
const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
12103

13-
// Configure the AWS CLI and AWS SDKs using environment variables
14-
15-
// AWS_ACCESS_KEY_ID:
16-
// Specifies an AWS access key associated with an IAM user or role
17-
core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId);
18-
19-
// AWS_SECRET_ACCESS_KEY:
20-
// Specifies the secret key associated with the access key. This is essentially the "password" for the access key.
21-
core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey);
22-
23-
// AWS_SESSION_TOKEN:
24-
// Specifies the session token value that is required if you are using temporary security credentials.
25-
if (sessionToken) {
26-
core.exportVariable('AWS_SESSION_TOKEN', sessionToken);
104+
// Get role credentials if configured to do so
105+
if (roleToAssume) {
106+
const roleCredentials = await assumeRole(
107+
{accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds}
108+
);
109+
exportCredentials(roleCredentials);
110+
} else {
111+
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
27112
}
28113

29-
// AWS_DEFAULT_REGION and AWS_REGION:
30-
// Specifies the AWS Region to send requests to
31-
core.exportVariable('AWS_DEFAULT_REGION', region);
32-
core.exportVariable('AWS_REGION', region);
33-
34-
// Get the AWS account ID
35-
const sts = new aws.STS({
36-
customUserAgent: 'configure-aws-credentials-for-github-actions'
37-
});
38-
const identity = await sts.getCallerIdentity().promise();
39-
const accountId = identity.Account;
40-
core.setOutput('aws-account-id', accountId);
41-
if (!maskAccountId || maskAccountId.toLowerCase() == 'true') {
42-
core.setSecret(accountId);
43-
}
114+
exportRegion(region);
115+
116+
await exportAccountId(maskAccountId);
44117
}
45118
catch (error) {
46119
core.setFailed(error.message);
120+
121+
const showStackTrace = process.env.SHOW_STACK_TRACE;
122+
123+
if (showStackTrace === 'true') {
124+
throw(error)
125+
}
126+
47127
}
48128
}
49129

0 commit comments

Comments
 (0)