Skip to content

feat: Allow custom session tags to be passed when assuming a role #1315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.history
node_modules
coverage
.DS_Store
Expand Down
4 changes: 3 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name: '"Configure AWS Credentials" Action for GitHub Actions'
description: Configures AWS credentials for use in subsequent steps in a GitHub Action workflow
runs:
Expand Down Expand Up @@ -75,6 +74,9 @@ inputs:
required: false
use-existing-credentials:
description: When enabled, this option will check if there are already valid credentials in the environment. If there are, new credentials will not be fetched. If there are not, the action will run as normal.
custom-tags:
description: Additional tags to apply to the assumed role session. Must be a JSON object provided as a string.
required: false
outputs:
aws-account-id:
description: The AWS account ID for the provided credentials
Expand Down
1 change: 1 addition & 0 deletions dist/cleanup/assumeRole.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion src/assumeRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface assumeRoleParams {
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: { arn: string }[];
customTags?: string;
}

export async function assumeRole(params: assumeRoleParams) {
Expand All @@ -91,6 +92,7 @@ export async function assumeRole(params: assumeRoleParams) {
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
} = { ...params };

// Load GitHub environment variables
Expand All @@ -108,17 +110,35 @@ export async function assumeRole(params: assumeRoleParams) {
{ Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) },
{ Key: 'Commit', Value: GITHUB_SHA },
];

if (process.env.GITHUB_REF) {
tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
}

if (customTags) {
try {
const parsed = JSON.parse(customTags);

// Then do the mapping
const newTags = Object.entries(parsed).map(([Key, Value]) => ({
Key,
Value: String(Value),
}));

tagArray.push(...newTags);
} catch {
throw new Error('Invalid custom-tags, json is not valid');
}
}

const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) {
core.debug('Role session tagging has been skipped.');
} else {
core.debug(`${tags.length} role session tags are being used.`);
core.debug(`${tags.length} role session tags are being used:`);
}

// Calculate role ARN from name and account ID (currently only supports `aws` partition)
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export async function run() {
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const proxyServer = core.getInput('http-proxy', { required: false });
const customTags = core.getInput('custom-tags', { required: false });

const inlineSessionPolicy = core.getInput('inline-session-policy', {
required: false,
});
Expand Down Expand Up @@ -184,6 +186,7 @@ export async function run() {
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
});
},
!disableRetry,
Expand Down
40 changes: 38 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,41 @@ describe('Configure AWS Credentials', {}, () => {
});
});

describe('Custom Tags', {}, () => {
beforeEach(() => {
mockedSTSClient.on(AssumeRoleCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials')
.mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' })
.mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' });
});
it('rejects invalid JSON in custom tags', {}, async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_INVALID_JSON_INPUTS));
await run();
expect(core.setFailed).toHaveBeenCalledWith('Invalid custom-tags, json is not valid');
//expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
});
it('handles object custom tags', {}, async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_OBJECT_INPUTS));
await run();
expect(core.info).toHaveBeenCalledWith('Assuming role with user credentials');
expect(core.info).toHaveBeenCalledWith('Authenticated as assumedRoleId AROAFAKEASSUMEDROLEID');
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)[0].args[0].input).toMatchObject({
Tags: expect.arrayContaining([
{ Key: 'GitHub', Value: 'Actions' },
{ Key: 'Repository', Value: 'MY-REPOSITORY-NAME' },
{ Key: 'Workflow', Value: 'MY-WORKFLOW-ID' },
{ Key: 'Action', Value: 'MY-ACTION-NAME' },
{ Key: 'Actor', Value: 'MY-USERNAME_bot_' },
{ Key: 'Commit', Value: 'MY-COMMIT-ID' },
{ Key: 'Environment', Value: 'Production' },
{ Key: 'Team', Value: 'DevOps' },
])
});
});
});

describe('Odd inputs', {}, () => {
it('fails when github env vars are missing', {}, async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.IAM_USER_INPUTS));
Expand All @@ -251,7 +286,7 @@ describe('Configure AWS Credentials', {}, () => {
await run();
expect(core.setFailed).toHaveBeenCalled();
});
it('does not fail if GITHUB_REF is missing', {}, async () => {
it('does not fail if GITHUB_REF is missing', {},async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.IAM_USER_INPUTS));
mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
Expand Down Expand Up @@ -311,6 +346,7 @@ describe('Configure AWS Credentials', {}, () => {
mockedSTSClient.on(GetCallerIdentityCommand).resolves(mocks.outputs.GET_CALLER_IDENTITY);
await run();
expect(core.setFailed).not.toHaveBeenCalled();
})
});
});
});

23 changes: 23 additions & 0 deletions test/mockinputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ const inputs = {
'aws-region': 'fake-region-1',
'special-characters-workaround': 'true',
},
CUSTOM_TAGS_JSON_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'custom-tags': '{"Environment": "Production", "Team": "DevOps"}',
},
CUSTOM_TAGS_INVALID_JSON_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': 'not a json',
},
CUSTOM_TAGS_OBJECT_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': JSON.stringify({ Environment: 'Production', Team: 'DevOps' }),
},
IAM_USER_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
Expand Down
Loading