Skip to content

Commit

Permalink
Allow inline session policies for assuming role (#739)
Browse files Browse the repository at this point in the history
* Allow to pass inline session policy as a parameter

Update the action file

Regenerate the dist/ content

Add test

* Fix typos

* Fix stylistic error

* Move the inline policy logic to allow assumeRole to use it as well; Update and add tests

* Add an option for managed policies

* Regenerate the dist/ files

* Use multiline input for managed policies

* Update readme

* Update readme

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
dimitar-hristov and mergify[bot] committed Jun 14, 2023
1 parent ae73407 commit d00f6c6
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 10 deletions.
43 changes: 43 additions & 0 deletions README.md
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Expand Up @@ -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'
Expand Down
28 changes: 23 additions & 5 deletions dist/index.js
Expand Up @@ -49229,7 +49229,9 @@ async function assumeRole(params) {
region,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies
} = params;
assert(
[roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`);
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
28 changes: 23 additions & 5 deletions index.js
Expand Up @@ -29,7 +29,9 @@ async function assumeRole(params) {
region,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies
} = params;
assert(
[roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`);
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions index.test.js
Expand Up @@ -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();
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit d00f6c6

Please sign in to comment.