From 1c435bbd5e1f1d36cdd703da5c4d6ee1ad91efac Mon Sep 17 00:00:00 2001 From: Clare Liguori Date: Mon, 2 Mar 2020 18:11:27 -0800 Subject: [PATCH] feat: Add option to provide external ID (#32) Fixes #28 --- README.md | 10 ++++++---- action.yml | 13 +++++++++++-- index.js | 25 +++++++++++++++++++++---- index.test.js | 25 ++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7b0361a1d..c2f90d9b9 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/I * [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) of the credentials used in GitHub Actions workflows. ## Assuming a role -If you would like to use the credentials you provide to this action to assume a role, you can do so by specifying the role ARN in `role-to-assume`. -The role credentials will then be output instead of the ones you have provided. -The default session duration is 6 hours, but if you would like to adjust this you can pass a duration to `role-duration-seconds`. +If you would like to use the static credentials you provide to this action to assume a role, you can do so by specifying the role ARN in `role-to-assume`. +The role credentials will then be configured in the Actions environment instead of the static credentials you have provided. +The default session duration is 6 hours, but if you would like to adjust this you can pass a duration to `role-duration-seconds`. The default session name is GitHubActions, and you can modify it by specifying the desired name in `role-session-name`. Example: @@ -64,10 +64,12 @@ Example: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-2 - role-to-assume: arn:aws:iam::123456789100:role/role-to-assume + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }} role-duration-seconds: 1200 role-session-name: MySessionName ``` +In this example, the secret `AWS_ROLE_TO_ASSUME` contains a string like `arn:aws:iam::123456789100:role/role-to-assume`. ### Session tagging The session will have the name "GitHubActions" and be tagged with the following tags: diff --git a/action.yml b/action.yml index b3dd47086..96d305861 100644 --- a/action.yml +++ b/action.yml @@ -17,10 +17,16 @@ inputs: description: 'AWS Region, e.g. us-east-2' required: true 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" + 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" + description: >- + Use the provided credentials to assume an IAM role and configure the Actions + environment with the assumed role credentials rather than with the provided + credentials required: false role-duration-seconds: description: "Role duration in seconds (default: 6 hours)" @@ -28,6 +34,9 @@ inputs: role-session-name: description: 'Role session name (default: GitHubActions)' required: false + role-external-id: + description: 'The external ID of the role to assume' + required: false outputs: aws-account-id: description: 'The AWS account ID for the provided credentials' diff --git a/index.js b/index.js index d525bced7..7b3582461 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,16 @@ async function assumeRole(params) { // Assume a role to get short-lived credentials using longer-lived credentials. const isDefined = i => !!i; - const {roleToAssume, roleDurationSeconds, roleSessionName, accessKeyId, secretAccessKey, sessionToken, region} = params; + const { + roleToAssume, + roleExternalId, + roleDurationSeconds, + roleSessionName, + accessKeyId, + secretAccessKey, + sessionToken, + region, + } = params; assert( [roleToAssume, roleDurationSeconds, roleSessionName, accessKeyId, secretAccessKey, region].every(isDefined), "Missing required input when assuming a Role." @@ -32,7 +41,8 @@ async function assumeRole(params) { const sts = new aws.STS({ accessKeyId, secretAccessKey, sessionToken, region, endpoint, customUserAgent: USER_AGENT }); - return sts.assumeRole({ + + const assumeRoleRequest = { RoleArn: roleToAssume, RoleSessionName: roleSessionName, DurationSeconds: roleDurationSeconds, @@ -45,7 +55,13 @@ async function assumeRole(params) { {Key: 'Branch', Value: GITHUB_REF}, {Key: 'Commit', Value: GITHUB_SHA}, ] - }) + }; + + if (roleExternalId) { + assumeRoleRequest.ExternalId = roleExternalId; + } + + return sts.assumeRole(assumeRoleRequest) .promise() .then(function (data) { return { @@ -121,13 +137,14 @@ async function run() { 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 roleExternalId = core.getInput('role-external-id', { required: false }); const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME; const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; // Get role credentials if configured to do so if (roleToAssume) { const roleCredentials = await assumeRole( - {accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleDurationSeconds, roleSessionName} + {accessKeyId, secretAccessKey, sessionToken, region, roleToAssume, roleExternalId, roleDurationSeconds, roleSessionName} ); exportCredentials(roleCredentials); } else { diff --git a/index.test.js b/index.test.js index 151c179d6..85cec53c0 100644 --- a/index.test.js +++ b/index.test.js @@ -260,11 +260,34 @@ describe('Configure AWS Credentials', () => { }) }); + test('role external ID provided', async () => { + core.getInput = jest + .fn() + .mockImplementation(mockGetInput({...ASSUME_ROLE_INPUTS, 'role-external-id': 'abcdef'})); + + 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: GITHUB_ACTOR_SANITIZED}, + {Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF}, + {Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA}, + ], + ExternalId: 'abcdef' + }) + }); + test('workflow name sanitized in role assumption tags', async () => { core.getInput = jest .fn() .mockImplementation(mockGetInput(ASSUME_ROLE_INPUTS)); - + process.env = {...process.env, GITHUB_WORKFLOW: 'Workflow!"#$%&\'()*+, -./:;<=>?@[]^_`{|}~🙂💥🍌1yFvMOeD3ZHYsHrGjCceOboMYzBPo0CRNFdcsVRG6UgR3A912a8KfcBtEVvkAS7kRBq80umGff8mux5IN1y55HQWPNBNyaruuVr4islFXte4FDQZexGJRUSMyHQpxJ8OmZnET84oDmbvmIjgxI6IBrdihX9PHMapT4gQvRYnLqNiKb18rEMWDNoZRy51UPX5sWK2GKPipgKSO9kqLckZai9D2AN2RlWCxtMqChNtxuxjqeqhoQZo0oaq39sjcRZgAAAAAAA'}; const sanitizedWorkflowName = 'Workflow__________+, -./:;<=>?@____________1yFvMOeD3ZHYsHrGjCceOboMYzBPo0CRNFdcsVRG6UgR3A912a8KfcBtEVvkAS7kRBq80umGff8mux5IN1y55HQWPNBNyaruuVr4islFXte4FDQZexGJRUSMyHQpxJ8OmZnET84oDmbvmIjgxI6IBrdihX9PHMapT4gQvRYnLqNiKb18rEMWDNoZRy51UPX5sWK2GKPipgKSO9kqLckZa'