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

Issue:
Optional AWS_ACCESS_KEY_ID and AWS_SECRET_ACCES_KEY when relying on IAM Roles
I'm using my own self-hosted runner which runs in AWS. The runner instance has IAM Role which takes care of the all the credentials and policies so I don't need to store any AWS credentials outside AWS.

The IAM role allows the runner to do a 'aws sts assume-role' but doesn't need any credentials (uses IAM role). 
I would like to use  this Github Action to do the assume-role, instead of scripting it myself and set the credential output as environment variable so I can use them in all the following steps, but currently AWS_ACCESS_KEY_ID and AWS_SECRET_ACCES_KEY are required. I would like to them to be optional so it fits the use case when using IAM roles which is a best-practice.

Pull request description:
feat: don't require access key credentials for self-hosted runners
Fixes #33 


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 6ca2446..cdf9ba0 100644
--- a/README.md
+++ b/README.md
@@ -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 '*'._
 
+## 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
 
 This code is made available under the MIT license.
diff --git a/action.yml b/action.yml
index 96d3058..2e3b8f2 100644
--- a/action.yml
+++ b/action.yml
@@ -5,11 +5,17 @@ branding:
   color: 'orange'
 inputs:
   aws-access-key-id:
-    description: 'AWS Access Key ID'
-    required: true
+    description: >-
+      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:
-    description: 'AWS Secret Access Key'
-    required: true
+    description: >-
+      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:
     description: 'AWS Session Token'
     required: false
diff --git a/index.js b/index.js
index b24d1a2..b4455d9 100644
--- a/index.js
+++ b/index.js
@@ -141,8 +141,8 @@ function getStsClient(region) {
 async function run() {
   try {
     // Get inputs
-    const accessKeyId = core.getInput('aws-access-key-id', { required: true });
-    const secretAccessKey = core.getInput('aws-secret-access-key', { required: true });
+    const accessKeyId = core.getInput('aws-access-key-id', { required: false });
+    const secretAccessKey = core.getInput('aws-secret-access-key', { required: false });
     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 });
@@ -151,13 +151,21 @@ async function run() {
     const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
     const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
 
+    exportRegion(region);
+
     // Always export the source credentials and account ID.
     // The STS client for calling AssumeRole pulls creds from the environment.
     // 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
     // in any error messages.
-    exportRegion(region);
-    exportCredentials({accessKeyId, secretAccessKey, sessionToken});
+    if (accessKeyId) {
+      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);
 
     // Get role credentials if configured to do so


Test patch:
diff --git a/index.test.js b/index.test.js
index 51c58ef..f3f24aa 100644
--- a/index.test.js
+++ b/index.test.js
@@ -32,17 +32,17 @@ function mockGetInput(requestResponse) {
         return requestResponse[name]
     }
 }
-const REQUIRED_INPUTS = {
+const CREDS_INPUTS = {
     'aws-access-key-id': FAKE_ACCESS_KEY_ID,
     'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY
 };
 const DEFAULT_INPUTS = {
-    ...REQUIRED_INPUTS,
+    ...CREDS_INPUTS,
     'aws-session-token': FAKE_SESSION_TOKEN,
     'aws-region': FAKE_REGION,
     '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 mockStsAssumeRole = jest.fn();
@@ -118,8 +118,24 @@ describe('Configure AWS Credentials', () => {
         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 () => {
-        const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'eu-west-1'};
+        const mockInputs = {...CREDS_INPUTS, 'aws-region': 'eu-west-1'};
         core.getInput = jest
             .fn()
             .mockImplementation(mockGetInput(mockInputs));
@@ -139,7 +155,7 @@ describe('Configure AWS Credentials', () => {
     });
 
     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
             .fn()
             .mockImplementation(mockGetInput(mockInputs));
@@ -218,6 +234,36 @@ describe('Configure AWS Credentials', () => {
         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 () => {
         core.getInput = jest
             .fn()
@@ -287,7 +333,7 @@ describe('Configure AWS Credentials', () => {
     test('role name provided instead of ARN', async () => {
         core.getInput = jest
             .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();
         expect(mockStsAssumeRole).toHaveBeenCalledWith({