Repository: aws-actions/configure-aws-credentials (ts)

Issue:
Option to set external-id when assuming role
For cases where roles can only be assumed on condition an expected external-id is passed in.

Pull request description:
feat: Add option to provide external ID
Fixes #28

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


Patch:
diff --git a/README.md b/README.md
index 7b0361a..c2f90d9 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 b3dd470..96d3058 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 d525bce..7b35824 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 {


Test patch:
diff --git a/index.test.js b/index.test.js
index 151c179..85cec53 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'