Repository: aws-actions/configure-aws-credentials
Pull request #739 (2023-05-31 23:27:55)

Allow inline session policies for assuming role
*Issue #229, if available:*

*Description of changes:*
- Allows inline and managed session policies as inputs.
- Regenerated the `dist/` files.
- Added test.
- Some relevant docs:
  - [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters)
  - [AssumeRole AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html#assumeRole-property)
  - [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) 
  - [AssumeRoleWithWebIdentity AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html#assumeRoleWithWebIdentity-property) 

---

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws-actions/configure-aws-credentials/blob/main/CONTRIBUTING.md)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


dimitar-hristov: Hi @peterwoodworth 👋 Thank you for the feedback! I have updated the PR to:
- Include a more generic implementation of the inline session policy that will now include the `assumeRole` as well as the use of OIDC web identity file.
- Include the implementation for managed session policies.
- Update the README to include examples.

Please let me know how it looks and if it makes sense 🙂 

Patch:
diff --git a/README.md b/README.md
index d3a9afc..5c487f4 100644
--- a/README.md
+++ b/README.md
@@ -320,6 +320,49 @@ within the Action. You can skip this session tagging by providing
         role-skip-session-tagging: true
 ```
 
+### Inline session policy
+An IAM policy in stringified JSON format that you want to use as an inline session policy.
+Depending on preferences, the JSON could be written on a single line like this:
+```yaml
+      uses: aws-actions/configure-aws-credentials@v2
+      with:
+         inline-session-policy: '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:List*","Resource":"*"}]}'
+```
+Or we can have a nicely formatted JSON as well:
+```yaml
+      uses: aws-actions/configure-aws-credentials@v2
+      with:
+         inline-session-policy: >-
+          {
+           "Version": "2012-10-17",
+           "Statement": [
+            {
+             "Sid":"Stmt1",
+             "Effect":"Allow",
+             "Action":"s3:List*",
+             "Resource":"*"
+            }
+           ]
+          }
+```
+
+### Managed session policies
+The Amazon Resource Names (ARNs) of the IAM managed policies that you want to use as managed session policies.
+The policies must exist in the same account as the role. You can pass a single managed policy like this:
+```yaml
+      uses: aws-actions/configure-aws-credentials@v2
+      with:
+         managed-session-policies: arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
+```
+And we can pass multiple managed policies likes this:
+```yaml
+      uses: aws-actions/configure-aws-credentials@v2
+      with:
+         managed-session-policies: |
+          arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
+          arn:aws:iam::aws:policy/AmazonS3OutpostsReadOnlyAccess
+```
+
 ## Self-Hosted Runners
 
 If you run your GitHub Actions in a
diff --git a/action.yml b/action.yml
index ef82373..7b1fc81 100644
--- a/action.yml
+++ b/action.yml
@@ -61,6 +61,12 @@ inputs:
   role-chaining:
     description: 'Use existing credentials from the environment to assume a new role'
     required: false
+  inline-session-policy:
+    description: 'Inline session policy'
+    required: false
+  managed-session-policies:
+    description: 'List of managed session policies'
+    required: false
 outputs:
   aws-account-id:
     description: 'The AWS account ID for the provided credentials'
diff --git a/dist/index.js b/dist/index.js
index b278da1..f4539da 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -49229,7 +49229,9 @@ async function assumeRole(params) {
     region,
     roleSkipSessionTagging,
     webIdentityTokenFile,
-    webIdentityToken
+    webIdentityToken,
+    inlineSessionPolicy,
+    managedSessionPolicies
   } = params;
   assert(
       [roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
@@ -49286,6 +49288,18 @@ async function assumeRole(params) {
     assumeRoleRequest.ExternalId = roleExternalId;
   }
 
+  if (isDefined(inlineSessionPolicy)) {
+    assumeRoleRequest.Policy = inlineSessionPolicy;
+  }
+
+  if (managedSessionPolicies && managedSessionPolicies.length) {
+    const policyArns = []
+    for (const managedSessionPolicy of managedSessionPolicies) {
+      policyArns.push({arn: managedSessionPolicy})
+    }
+    assumeRoleRequest.PolicyArns = policyArns;
+  }
+
   let assumeFunction = sts.assumeRole.bind(sts);
 
   // These are customizations needed for the GH OIDC Provider
@@ -49505,6 +49519,8 @@ async function run() {
     const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
     const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
     const proxyServer = core.getInput('http-proxy', { required: false });
+    const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
+    const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false })
 
     if (!region.match(REGION_REGEX)) {
       throw new Error(`Region is not valid: ${region}`);
@@ -49513,12 +49529,12 @@ async function run() {
     exportRegion(region);
 
     // This wraps the logic for deciding if we should rely on the GH OIDC provider since we may need to reference
-    // the decision in a few differennt places. Consolidating it here makes the logic clearer elsewhere.
+    // the decision in a few different places. Consolidating it here makes the logic clearer elsewhere.
     const useGitHubOIDCProvider = () => {
         // The assumption here is that self-hosted runners won't be populating the `ACTIONS_ID_TOKEN_REQUEST_TOKEN`
-        // environment variable and they won't be providing a web idenity token file or access key either.
+        // environment variable, and they won't be providing a web identity token file or access key either.
         // V2 of the action might relax this a bit and create an explicit precedence for these so that customers
-        // can provide as much info as they want and we will follow the established credential loading precedence.
+        // can provide as much info as they want, and we will follow the established credential loading precedence.
 
         return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile && !roleChaining
     }
@@ -49571,7 +49587,9 @@ async function run() {
             roleSessionName,
             roleSkipSessionTagging,
             webIdentityTokenFile,
-            webIdentityToken
+            webIdentityToken,
+            inlineSessionPolicy,
+            managedSessionPolicies
           }) }, true);
       exportCredentials(roleCredentials);
       // We need to validate the credentials in 2 of our use-cases
diff --git a/index.js b/index.js
index 4180639..a67529d 100644
--- a/index.js
+++ b/index.js
@@ -29,7 +29,9 @@ async function assumeRole(params) {
     region,
     roleSkipSessionTagging,
     webIdentityTokenFile,
-    webIdentityToken
+    webIdentityToken,
+    inlineSessionPolicy,
+    managedSessionPolicies
   } = params;
   assert(
       [roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
@@ -86,6 +88,18 @@ async function assumeRole(params) {
     assumeRoleRequest.ExternalId = roleExternalId;
   }
 
+  if (isDefined(inlineSessionPolicy)) {
+    assumeRoleRequest.Policy = inlineSessionPolicy;
+  }
+
+  if (managedSessionPolicies && managedSessionPolicies.length) {
+    const policyArns = []
+    for (const managedSessionPolicy of managedSessionPolicies) {
+      policyArns.push({arn: managedSessionPolicy})
+    }
+    assumeRoleRequest.PolicyArns = policyArns;
+  }
+
   let assumeFunction = sts.assumeRole.bind(sts);
 
   // These are customizations needed for the GH OIDC Provider
@@ -305,6 +319,8 @@ async function run() {
     const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
     const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
     const proxyServer = core.getInput('http-proxy', { required: false });
+    const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
+    const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false })
 
     if (!region.match(REGION_REGEX)) {
       throw new Error(`Region is not valid: ${region}`);
@@ -313,12 +329,12 @@ async function run() {
     exportRegion(region);
 
     // This wraps the logic for deciding if we should rely on the GH OIDC provider since we may need to reference
-    // the decision in a few differennt places. Consolidating it here makes the logic clearer elsewhere.
+    // the decision in a few different places. Consolidating it here makes the logic clearer elsewhere.
     const useGitHubOIDCProvider = () => {
         // The assumption here is that self-hosted runners won't be populating the `ACTIONS_ID_TOKEN_REQUEST_TOKEN`
-        // environment variable and they won't be providing a web idenity token file or access key either.
+        // environment variable, and they won't be providing a web identity token file or access key either.
         // V2 of the action might relax this a bit and create an explicit precedence for these so that customers
-        // can provide as much info as they want and we will follow the established credential loading precedence.
+        // can provide as much info as they want, and we will follow the established credential loading precedence.
 
         return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile && !roleChaining
     }
@@ -371,7 +387,9 @@ async function run() {
             roleSessionName,
             roleSkipSessionTagging,
             webIdentityTokenFile,
-            webIdentityToken
+            webIdentityToken,
+            inlineSessionPolicy,
+            managedSessionPolicies
           }) }, true);
       exportCredentials(roleCredentials);
       // We need to validate the credentials in 2 of our use-cases


Test patch:
diff --git a/index.test.js b/index.test.js
index e6655ad..0686b5e 100644
--- a/index.test.js
+++ b/index.test.js
@@ -45,6 +45,7 @@ const DEFAULT_INPUTS = {
     'aws-region': FAKE_REGION,
     'mask-aws-account-id': 'TRUE'
 };
+const DEFAULT_MULTILINE_INPUTS = {}
 const ASSUME_ROLE_INPUTS = {...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION};
 
 const mockStsCallerIdentity = jest.fn();
@@ -90,6 +91,10 @@ describe('Configure AWS Credentials', () => {
             .fn()
             .mockImplementation(mockGetInput(DEFAULT_INPUTS));
 
+        core.getMultilineInput = jest
+            .fn()
+            .mockImplementation(mockGetInput(DEFAULT_MULTILINE_INPUTS));
+
         core.getIDToken = jest
             .fn()
             .mockImplementation(() => {
@@ -624,6 +629,49 @@ describe('Configure AWS Credentials', () => {
         })
     });
 
+    test('Web identity token file with a inline session policy', async () => {
+        const CUSTOM_SESSION_POLICY = "{ super_secure_policy }";
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': '/fake/token/file', 'inline-session-policy': CUSTOM_SESSION_POLICY}));
+
+        await run();
+        expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
+            RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
+            RoleSessionName: 'GitHubActions',
+            DurationSeconds: 6 * 3600,
+            Policy: CUSTOM_SESSION_POLICY,
+            WebIdentityToken: 'testpayload'
+        })
+        expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCOUNT_ID);
+        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);
+    });
+
+    test('Web identity token file with a managed session policies', async () => {
+        const MANAGED_SESSION_POLICIES = ["arn:aws:iam::111111111111:policy/foo", "arn:aws:iam::111111111111:policy/bar"];
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': '/fake/token/file'}));
+        core.getMultilineInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'managed-session-policies': MANAGED_SESSION_POLICIES}))
+
+        await run();
+        expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
+            RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
+            RoleSessionName: 'GitHubActions',
+            DurationSeconds: 6 * 3600,
+            PolicyArns: [{arn: MANAGED_SESSION_POLICIES[0]}, {arn: MANAGED_SESSION_POLICIES[1]}],
+            WebIdentityToken: 'testpayload'
+        })
+        expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCOUNT_ID);
+        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);
+    });
+
     test('only role arn and region provided to use GH OIDC Token', async () => {
         process.env.GITHUB_ACTIONS = 'true';
         process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token';
@@ -664,6 +712,51 @@ describe('Configure AWS Credentials', () => {
         expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN);
     });
 
+    test('GH OIDC With inline session policy', async () => {
+        const CUSTOM_SESSION_POLICY = "{ super_secure_policy }";
+        process.env.GITHUB_ACTIONS = 'true';
+        process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token';
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'inline-session-policy': CUSTOM_SESSION_POLICY}));
+
+        await run();
+        expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
+            RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
+            RoleSessionName: 'GitHubActions',
+            DurationSeconds: 3600,
+            Policy: CUSTOM_SESSION_POLICY,
+            WebIdentityToken: 'testtoken'
+        });
+        expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_STS_ACCESS_KEY_ID);
+        expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_SECRET_ACCESS_KEY);
+        expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN);
+    });
+
+    test('GH OIDC With managed session policy', async () => {
+        const MANAGED_SESSION_POLICIES = ["arn:aws:iam::111111111111:policy/foo", "arn:aws:iam::111111111111:policy/bar"];
+        process.env.GITHUB_ACTIONS = 'true';
+        process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token';
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION}));
+        core.getMultilineInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'managed-session-policies': MANAGED_SESSION_POLICIES}))
+
+        await run();
+        expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
+            RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
+            RoleSessionName: 'GitHubActions',
+            DurationSeconds: 3600,
+            PolicyArns: [{arn: MANAGED_SESSION_POLICIES[0]}, {arn: MANAGED_SESSION_POLICIES[1]}],
+            WebIdentityToken: 'testtoken'
+        });
+        expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_STS_ACCESS_KEY_ID);
+        expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_SECRET_ACCESS_KEY);
+        expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SESSION_TOKEN);
+    });
+
     test('role assumption fails after maximun trials using OIDC Provider', async () => {
         process.env.GITHUB_ACTIONS = 'true';
         process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token';
@@ -704,6 +797,57 @@ describe('Configure AWS Credentials', () => {
         })
     });
 
+    test('inline session policy provided', async () => {
+        const CUSTOM_SESSION_POLICY = "{ super_secure_policy }";
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({...ASSUME_ROLE_INPUTS, 'inline-session-policy': CUSTOM_SESSION_POLICY}));
+
+        await run();
+        expect(mockStsAssumeRole).toHaveBeenCalledWith({
+            RoleArn: ROLE_ARN,
+            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: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA},
+                {Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF},
+            ],
+            Policy: CUSTOM_SESSION_POLICY
+        })
+    });
+
+    test('managed session policy provided', async () => {
+        const MANAGED_SESSION_POLICIES = ["arn:aws:iam::111111111111:policy/foo", "arn:aws:iam::111111111111:policy/bar"];
+        core.getInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({...ASSUME_ROLE_INPUTS}));
+        core.getMultilineInput = jest
+            .fn()
+            .mockImplementation(mockGetInput({'managed-session-policies': MANAGED_SESSION_POLICIES}))
+
+        await run();
+        expect(mockStsAssumeRole).toHaveBeenCalledWith({
+            RoleArn: ROLE_ARN,
+            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: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA},
+                {Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF},
+            ],
+            PolicyArns: [{arn: MANAGED_SESSION_POLICIES[0]}, {arn: MANAGED_SESSION_POLICIES[1]}],
+        })
+    });
+
     test('workflow name sanitized in role assumption tags', async () => {
         core.getInput = jest
             .fn()