Compare commits

...

6 Commits

Author SHA1 Message Date
Release Automation cf6b87e99e chore(release): 1.3.1 2020-03-06 22:35:15 +00:00
GitHub Actions 5ce6d03987 chore: Update dist 2020-03-06 22:30:26 +00:00
Clare Liguori 3d568d2c43 fix: validate region input string (#44) 2020-03-06 14:29:46 -08:00
Release Automation 75aa8f1e41 chore(release): 1.3.0 2020-03-06 18:05:29 +00:00
GitHub Actions dee935fabb chore: Update dist 2020-03-06 18:00:07 +00:00
Clare Liguori a20ed60252 feat: don't require access key credentials for self-hosted runners (#42) 2020-03-06 17:59:39 +00:00
8 changed files with 148 additions and 20 deletions
+14
View File
@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.3.1](https://github.com/aws-actions/configure-aws-credentials/compare/v1.3.0...v1.3.1) (2020-03-06)
### Bug Fixes
* validate region input string ([#44](https://github.com/aws-actions/configure-aws-credentials/issues/44)) ([3d568d2](https://github.com/aws-actions/configure-aws-credentials/commit/3d568d2c4359304d46d9bd1b4d9f69e088ccbf7b))
## [1.3.0](https://github.com/aws-actions/configure-aws-credentials/compare/v1.2.0...v1.3.0) (2020-03-06)
### Features
* don't require access key credentials for self-hosted runners ([#42](https://github.com/aws-actions/configure-aws-credentials/issues/42)) ([a20ed60](https://github.com/aws-actions/configure-aws-credentials/commit/a20ed6025224ca999786c8d4e687f119cfedec65))
## [1.2.0](https://github.com/aws-actions/configure-aws-credentials/compare/v1.1.2...v1.2.0) (2020-03-06) ## [1.2.0](https://github.com/aws-actions/configure-aws-credentials/compare/v1.1.2...v1.2.0) (2020-03-06)
+23
View File
@@ -130,6 +130,29 @@ The session will have the name "GitHubActions" and be tagged with the following
_Note: all tag values must conform to [the requirements](https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html). Particularly, `GITHUB_WORKFLOW` will be truncated if it's too long. If `GITHUB_ACTOR` or `GITHUB_WORKFLOW` contain invalid charcters, the characters will be replaced with an '*'._ _Note: all tag values must conform to [the requirements](https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html). Particularly, `GITHUB_WORKFLOW` will be truncated if it's too long. If `GITHUB_ACTOR` or `GITHUB_WORKFLOW` contain invalid charcters, the characters will be replaced with an '*'._
## Self-hosted runners
If you run your GitHub Actions in a [self-hosted runner](https://help.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners) that already has access to AWS credentials, such as an EC2 instance, then you do not need to provide IAM user access key credentials to this action.
If no access key credentials are given in the action inputs, this action will use credentials from the runner environment using the [default methods for the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html).
You can use this action to simply configure the region and account ID in the environment, and then use the runner's credentials for all AWS API calls made by your Actions workflow:
```yaml
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: us-east-2
```
In this case, your runner's credentials must have permissions to call any AWS APIs called by your Actions workflow.
Or, you can use this action to assume a role, and then use the role credentials for all AWS API calls made by your Actions workflow:
```yaml
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: us-east-2
role-to-assume: my-github-actions-role
```
In this case, your runner's credentials must have permissions to assume the role.
## License Summary ## License Summary
This code is made available under the MIT license. This code is made available under the MIT license.
+10 -4
View File
@@ -5,11 +5,17 @@ branding:
color: 'orange' color: 'orange'
inputs: inputs:
aws-access-key-id: aws-access-key-id:
description: 'AWS Access Key ID' description: >-
required: true AWS Access Key ID. This input is required if running in the GitHub hosted environment.
It is optional if running in a self-hosted environment that already has AWS credentials,
for example on an EC2 instance.
required: false
aws-secret-access-key: aws-secret-access-key:
description: 'AWS Secret Access Key' description: >-
required: true AWS Secret Access Key. This input is required if running in the GitHub hosted environment.
It is optional if running in a self-hosted environment that already has AWS credentials,
for example on an EC2 instance.
required: false
aws-session-token: aws-session-token:
description: 'AWS Session Token' description: 'AWS Session Token'
required: false required: false
+17 -4
View File
@@ -137,6 +137,7 @@ const USER_AGENT = 'configure-aws-credentials-for-github-actions';
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_'; const SANITIZATION_CHARACTER = '_';
const ROLE_SESSION_NAME = 'GitHubActions'; const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
async function assumeRole(params) { async function assumeRole(params) {
// Assume a role to get short-lived credentials using longer-lived credentials. // Assume a role to get short-lived credentials using longer-lived credentials.
@@ -268,8 +269,8 @@ function getStsClient(region) {
async function run() { async function run() {
try { try {
// Get inputs // Get inputs
const accessKeyId = core.getInput('aws-access-key-id', { required: true }); const accessKeyId = core.getInput('aws-access-key-id', { required: false });
const secretAccessKey = core.getInput('aws-secret-access-key', { required: true }); const secretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const region = core.getInput('aws-region', { required: true }); const region = core.getInput('aws-region', { required: true });
const sessionToken = core.getInput('aws-session-token', { required: false }); const sessionToken = core.getInput('aws-session-token', { required: false });
const maskAccountId = core.getInput('mask-aws-account-id', { required: false }); const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
@@ -278,13 +279,25 @@ async function run() {
const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME; const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
exportRegion(region);
// Always export the source credentials and account ID. // Always export the source credentials and account ID.
// The STS client for calling AssumeRole pulls creds from the environment. // The STS client for calling AssumeRole pulls creds from the environment.
// Plus, in the assume role case, if the AssumeRole call fails, we want // Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials and accound ID to already be masked as secrets // the source credentials and accound ID to already be masked as secrets
// in any error messages. // in any error messages.
exportRegion(region); if (accessKeyId) {
exportCredentials({accessKeyId, secretAccessKey, sessionToken}); if (!secretAccessKey) {
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");
}
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
}
const sourceAccountId = await exportAccountId(maskAccountId, region); const sourceAccountId = await exportAccountId(maskAccountId, region);
// Get role credentials if configured to do so // Get role credentials if configured to do so
+17 -4
View File
@@ -10,6 +10,7 @@ const USER_AGENT = 'configure-aws-credentials-for-github-actions';
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_'; const SANITIZATION_CHARACTER = '_';
const ROLE_SESSION_NAME = 'GitHubActions'; const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
async function assumeRole(params) { async function assumeRole(params) {
// Assume a role to get short-lived credentials using longer-lived credentials. // Assume a role to get short-lived credentials using longer-lived credentials.
@@ -141,8 +142,8 @@ function getStsClient(region) {
async function run() { async function run() {
try { try {
// Get inputs // Get inputs
const accessKeyId = core.getInput('aws-access-key-id', { required: true }); const accessKeyId = core.getInput('aws-access-key-id', { required: false });
const secretAccessKey = core.getInput('aws-secret-access-key', { required: true }); const secretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const region = core.getInput('aws-region', { required: true }); const region = core.getInput('aws-region', { required: true });
const sessionToken = core.getInput('aws-session-token', { required: false }); const sessionToken = core.getInput('aws-session-token', { required: false });
const maskAccountId = core.getInput('mask-aws-account-id', { required: false }); const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
@@ -151,13 +152,25 @@ async function run() {
const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME; const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
exportRegion(region);
// Always export the source credentials and account ID. // Always export the source credentials and account ID.
// The STS client for calling AssumeRole pulls creds from the environment. // The STS client for calling AssumeRole pulls creds from the environment.
// Plus, in the assume role case, if the AssumeRole call fails, we want // Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials and accound ID to already be masked as secrets // the source credentials and accound ID to already be masked as secrets
// in any error messages. // in any error messages.
exportRegion(region); if (accessKeyId) {
exportCredentials({accessKeyId, secretAccessKey, sessionToken}); if (!secretAccessKey) {
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");
}
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
}
const sourceAccountId = await exportAccountId(maskAccountId, region); const sourceAccountId = await exportAccountId(maskAccountId, region);
// Get role credentials if configured to do so // Get role credentials if configured to do so
+65 -6
View File
@@ -32,17 +32,17 @@ function mockGetInput(requestResponse) {
return requestResponse[name] return requestResponse[name]
} }
} }
const REQUIRED_INPUTS = { const CREDS_INPUTS = {
'aws-access-key-id': FAKE_ACCESS_KEY_ID, 'aws-access-key-id': FAKE_ACCESS_KEY_ID,
'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY 'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY
}; };
const DEFAULT_INPUTS = { const DEFAULT_INPUTS = {
...REQUIRED_INPUTS, ...CREDS_INPUTS,
'aws-session-token': FAKE_SESSION_TOKEN, 'aws-session-token': FAKE_SESSION_TOKEN,
'aws-region': FAKE_REGION, 'aws-region': FAKE_REGION,
'mask-aws-account-id': 'TRUE' 'mask-aws-account-id': 'TRUE'
}; };
const ASSUME_ROLE_INPUTS = {...REQUIRED_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION}; const ASSUME_ROLE_INPUTS = {...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION};
const mockStsCallerIdentity = jest.fn(); const mockStsCallerIdentity = jest.fn();
const mockStsAssumeRole = jest.fn(); const mockStsAssumeRole = jest.fn();
@@ -118,8 +118,24 @@ describe('Configure AWS Credentials', () => {
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
}); });
test('hosted runners can pull creds from a self-hosted environment', async () => {
const mockInputs = {'aws-region': FAKE_REGION};
core.getInput = jest
.fn()
.mockImplementation(mockGetInput(mockInputs));
await run();
expect(mockStsAssumeRole).toHaveBeenCalledTimes(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('session token is optional', async () => { test('session token is optional', async () => {
const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'eu-west-1'}; const mockInputs = {...CREDS_INPUTS, 'aws-region': 'eu-west-1'};
core.getInput = jest core.getInput = jest
.fn() .fn()
.mockImplementation(mockGetInput(mockInputs)); .mockImplementation(mockGetInput(mockInputs));
@@ -138,8 +154,21 @@ describe('Configure AWS Credentials', () => {
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
}); });
test('validates region name', async () => {
process.env.SHOW_STACK_TRACE = 'false';
const mockInputs = {...CREDS_INPUTS, 'aws-region': '$AWS_REGION'};
core.getInput = jest
.fn()
.mockImplementation(mockGetInput(mockInputs));
await run();
expect(core.setFailed).toHaveBeenCalledWith('Region is not valid: $AWS_REGION');
});
test('can opt out of masking account ID', async () => { test('can opt out of masking account ID', async () => {
const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false'}; const mockInputs = {...CREDS_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false'};
core.getInput = jest core.getInput = jest
.fn() .fn()
.mockImplementation(mockGetInput(mockInputs)); .mockImplementation(mockGetInput(mockInputs));
@@ -218,6 +247,36 @@ describe('Configure AWS Credentials', () => {
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID);
}); });
test('assume role can pull source credentials from self-hosted environment', async () => {
core.getInput = jest
.fn()
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION}));
await run();
expect(mockStsAssumeRole).toHaveBeenCalledTimes(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 () => { test('role assumption tags', async () => {
core.getInput = jest core.getInput = jest
.fn() .fn()
@@ -287,7 +346,7 @@ describe('Configure AWS Credentials', () => {
test('role name provided instead of ARN', async () => { test('role name provided instead of ARN', async () => {
core.getInput = jest core.getInput = jest
.fn() .fn()
.mockImplementation(mockGetInput({...REQUIRED_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION})); .mockImplementation(mockGetInput({...CREDS_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION}));
await run(); await run();
expect(mockStsAssumeRole).toHaveBeenCalledWith({ expect(mockStsAssumeRole).toHaveBeenCalledWith({
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aws-actions-configure-aws-credentials", "name": "aws-actions-configure-aws-credentials",
"version": "1.2.0", "version": "1.3.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aws-actions-configure-aws-credentials", "name": "aws-actions-configure-aws-credentials",
"version": "1.2.0", "version": "1.3.1",
"description": "Configure AWS Credentials", "description": "Configure AWS Credentials",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {