mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-02 05:55:10 +09:00
feat: Allow custom session tags to be passed when assuming a role (#1759)
* Add possibility to input custom session tags * Use json for input to custom-tags, add documentation for custom-tags * Add more examples * Simplify example to avoid parse error * Add input validation for custom tags * Fix unit tests for custom-tags * Add debugging message * Skip failing test for now * Build package * Remove some unused validation for custom tags * feat: add validation for custom session tags Harden the custom-tags feature against misuse and misconfiguration: - Validate input is a JSON object (reject arrays, primitives, null) - Enforce STS tag constraints: key length (128), value length (256), allowed characters - Reject nested object/array values that would silently stringify to '[object Object]' - Block overriding default session tags (GitHub, Repository, Workflow, etc.) - Enforce 50-tag session limit - Warn when custom-tags used with OIDC or web identity - Fix missing await on helpers test assertion - Remove unused CUSTOM_TAGS_JSON_INPUTS fixture - Normalize test mocking to vi.mocked() pattern --------- Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com>
This commit is contained in:
+81
-1
@@ -238,6 +238,85 @@ 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.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_INVALID_JSON_INPUTS));
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith('custom-tags: input is not valid JSON');
|
||||
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
|
||||
});
|
||||
it('handles valid custom tags', {}, async () => {
|
||||
vi.mocked(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' },
|
||||
]),
|
||||
});
|
||||
});
|
||||
it('rejects array input for custom tags', {}, async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_ARRAY_INPUTS));
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'custom-tags: input must be a JSON object (not an array or primitive)',
|
||||
);
|
||||
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
|
||||
});
|
||||
it('rejects custom tags that conflict with default session tags', {}, async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_RESERVED_KEY_INPUTS));
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
"custom-tags: key 'Repository' conflicts with a default session tag set by this action and cannot be overridden",
|
||||
);
|
||||
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
|
||||
});
|
||||
it('rejects custom tags with invalid key characters', {}, async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.CUSTOM_TAGS_INVALID_KEY_CHARS_INPUTS));
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
expect.stringContaining("custom-tags: key 'invalid{key}' contains invalid characters"),
|
||||
);
|
||||
expect(mockedSTSClient.commandCalls(AssumeRoleCommand)).toHaveLength(0);
|
||||
});
|
||||
it('warns when custom tags are used with OIDC', {}, async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(
|
||||
mocks.getInput({
|
||||
...mocks.GH_OIDC_INPUTS,
|
||||
'custom-tags': JSON.stringify({ MyTag: 'value' }),
|
||||
}),
|
||||
);
|
||||
vi.mocked(core.getIDToken).mockResolvedValue('testoidctoken');
|
||||
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).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').mockResolvedValue({
|
||||
accessKeyId: 'STSAWSACCESSKEYID',
|
||||
});
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
|
||||
await run();
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("'custom-tags' is set but will be ignored"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Odd inputs', {}, () => {
|
||||
it('fails when github env vars are missing', {}, async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.IAM_USER_INPUTS));
|
||||
@@ -269,6 +348,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles improper retry-max-attempts input', {}, async () => {
|
||||
// This should mean we retry one time
|
||||
vi.mocked(core.getInput).mockImplementation(
|
||||
@@ -712,7 +792,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
// Get the timeout callback function
|
||||
const timeoutCallback = setTimeoutSpy.mock.calls[0][0] as () => void;
|
||||
|
||||
|
||||
// Execute the timeout callback
|
||||
timeoutCallback();
|
||||
|
||||
|
||||
@@ -6,6 +6,46 @@ const inputs = {
|
||||
'aws-region': 'fake-region-1',
|
||||
'special-characters-workaround': 'true',
|
||||
},
|
||||
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_ARRAY_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': '[1, 2, 3]',
|
||||
},
|
||||
CUSTOM_TAGS_RESERVED_KEY_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({ Repository: 'evil-repo' }),
|
||||
},
|
||||
CUSTOM_TAGS_INVALID_KEY_CHARS_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({ 'invalid{key}': 'value' }),
|
||||
},
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user