mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-03-12 18:07:10 -04:00
Allow inline session policies for assuming role (#739)
* 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>
This commit is contained in:
43
README.md
43
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
|
||||
|
||||
@@ -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'
|
||||
|
||||
28
dist/index.js
vendored
28
dist/index.js
vendored
@@ -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
|
||||
|
||||
28
index.js
28
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
|
||||
|
||||
144
index.test.js
144
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()
|
||||
|
||||
Reference in New Issue
Block a user