diff --git a/README.md b/README.md index d3a9afc70..5c487f4c2 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 ef82373b5..7b1fc8147 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 b278da126..f4539daeb 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 418063980..a67529d96 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 diff --git a/index.test.js b/index.test.js index e6655adc7..0686b5edf 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()