From 6dd528eaec84838d826a0f321cfee4d8c22e917b Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Thu, 6 Feb 2025 15:25:50 -0800 Subject: [PATCH 1/5] Add functionality to re-use existing credentials --- action.yml | 2 ++ src/helpers.ts | 10 ++++++++++ src/index.ts | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/action.yml b/action.yml index 7e98591a9..155a48209 100644 --- a/action.yml +++ b/action.yml @@ -73,6 +73,8 @@ inputs: special-characters-workaround: description: Some environments do not support special characters in AWS_SECRET_ACCESS_KEY. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. This option is disabled by default required: false + use-existing-credentials: + description: Set to true if you are using multiple workflows that use the same AWS Credentials. 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. outputs: aws-account-id: description: The AWS account ID for the provided credentials diff --git a/src/helpers.ts b/src/helpers.ts index e27059649..fef371d78 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -144,3 +144,13 @@ export function isDefined(i: T | undefined | null): i is T { return i !== undefined && i !== null; } /* c8 ignore stop */ + +export async function areCredentialsValid(credentialsClient: CredentialsClient) { + const client = credentialsClient.stsClient; + const identity = await client.send(new GetCallerIdentityCommand({})); + const accountId = identity.Account; + if (!accountId) { + return false; + } + return true; +} diff --git a/src/index.ts b/src/index.ts index 46272e019..4bf3b9cfe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { retryAndBackoff, unsetCredentials, verifyKeys, + areCredentialsValid } from './helpers'; const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds) @@ -60,6 +61,7 @@ export async function run() { const specialCharacterWorkaroundInput = core.getInput('special-characters-workaround', { required: false }) || 'false'; const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true'; + const useExistingCredentials = core.getInput('use-existing-credentials', { required: false}) || 'false'; let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12; switch (true) { case specialCharacterWorkaround: @@ -75,6 +77,8 @@ export async function run() { managedSessionPolicies.push({ arn: managedSessionPolicy }); } + + // Logic to decide whether to attempt to use OIDC or not const useGitHubOIDCProvider = () => { // The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted. From 32b0eed9b3311912b7b8854a6c54001fea3b58aa Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Fri, 7 Feb 2025 10:58:49 -0800 Subject: [PATCH 2/5] Finish adding use-existing-credentials logic --- src/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4bf3b9cfe..d4f3eb0cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts'; import { CredentialsClient } from './CredentialsClient'; import { assumeRole } from './assumeRole'; import { + areCredentialsValid, errorMessage, exportAccountId, exportCredentials, @@ -10,7 +11,6 @@ import { retryAndBackoff, unsetCredentials, verifyKeys, - areCredentialsValid } from './helpers'; const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds) @@ -61,7 +61,7 @@ export async function run() { const specialCharacterWorkaroundInput = core.getInput('special-characters-workaround', { required: false }) || 'false'; const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true'; - const useExistingCredentials = core.getInput('use-existing-credentials', { required: false}) || 'false'; + const useExistingCredentials = core.getInput('use-existing-credentials', { required: false }) || 'false'; let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12; switch (true) { case specialCharacterWorkaround: @@ -77,8 +77,6 @@ export async function run() { managedSessionPolicies.push({ arn: managedSessionPolicy }); } - - // Logic to decide whether to attempt to use OIDC or not const useGitHubOIDCProvider = () => { // The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted. @@ -120,6 +118,17 @@ export async function run() { let sourceAccountId: string; let webIdentityToken: string; + //if the user wants to attempt to use existing credentials, check if we have some already + if (useExistingCredentials === 'true') { + core.info('I set the use-existing-credentials value to true!'); + const validCredentials = await areCredentialsValid(credentialsClient); + if (validCredentials) { + core.info('Pre-existing credentials are valid. No need to generate new ones.'); + return; + } + core.info('No valid credentials exist. Running as normal.'); + } + // If OIDC is being used, generate token // Else, export credentials provided as input if (useGitHubOIDCProvider()) { From 4c88881b41d3bd12a40ce83de54c9948cef52cb3 Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Fri, 7 Feb 2025 14:02:28 -0800 Subject: [PATCH 3/5] Add testing for use-existing-credentials --- src/index.ts | 1 - test/index.test.ts | 18 ++++++++++++++++++ test/mockinputs.test.ts | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d4f3eb0cd..01e8dce78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,7 +120,6 @@ export async function run() { //if the user wants to attempt to use existing credentials, check if we have some already if (useExistingCredentials === 'true') { - core.info('I set the use-existing-credentials value to true!'); const validCredentials = await areCredentialsValid(credentialsClient); if (validCredentials) { core.info('Pre-existing credentials are valid. No need to generate new ones.'); diff --git a/test/index.test.ts b/test/index.test.ts index e65b5071f..34e95687b 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -299,5 +299,23 @@ describe('Configure AWS Credentials', {}, () => { await run(); expect(core.setFailed).toHaveBeenCalled(); }); + it('re-uses existing credentials if they are valid', {}, async () => { + vi.clearAllMocks(); + mockedSTSClient.reset(); + vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.USE_EXISTING_CREDENTIALS_INPUTS)); + vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; + + mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS); + mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY_NO_CREDS }); + await run(); + expect(core.info).toHaveBeenCalledWith('No valid credentials exist. Running as normal.') + + mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); + + mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS); + await run(); + expect(core.info).toHaveBeenLastCalledWith('Pre-existing credentials are valid. No need to generate new ones.') + }) }); }); diff --git a/test/mockinputs.test.ts b/test/mockinputs.test.ts index 7773a92cc..f8ae6452b 100644 --- a/test/mockinputs.test.ts +++ b/test/mockinputs.test.ts @@ -27,6 +27,12 @@ const inputs = { 'role-chaining': 'true', 'aws-region': 'fake-region-1', }, + USE_EXISTING_CREDENTIALS_INPUTS: { + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', + 'aws-region': 'fake-region-1', + 'special-characters-workaround': 'true', + 'use-existing-credentials': 'true', + } }; const envs = { @@ -55,6 +61,9 @@ const outputs = { GET_CALLER_IDENTITY: { Account: '111111111111', Arn: 'arn:aws:iam::111111111111:role/MY-ROLE', + }, + GET_CALLER_IDENTITY_NO_CREDS: { + }, FAKE_STS_ACCESS_KEY_ID: 'STSAWSACCESSKEYID', FAKE_STS_SECRET_ACCESS_KEY: 'STSAWSSECRETACCESSKEY', From 7b1a6caeded094680b5c31b97f2a681e49073747 Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Fri, 7 Feb 2025 14:20:24 -0800 Subject: [PATCH 4/5] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 03f7a43e2..c3db941fd 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ See [action.yml](./action.yml) for more detail. | disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | No | | retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No | | special-characters-workaround | Uncommonly, some environments cannot tolerate special characters in a secret key. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. | No | +| use-existing-credentials | When set, the action will attempt to use existing credentials instead of fetching new credentials. Defaults to false. | No | #### Credential Lifetime The default session duration is **1 hour**. From e9f0490ed666b8100f8cd81f6b75dbf825d49814 Mon Sep 17 00:00:00 2001 From: Tom Keller Date: Fri, 7 Feb 2025 15:58:25 -0800 Subject: [PATCH 5/5] feat: finalize use-exisiting-credentials feature --- README.md | 2 +- action.yml | 2 +- src/helpers.ts | 11 +++++++---- src/index.ts | 9 +++++---- test/index.test.ts | 23 +++++++++-------------- test/mockinputs.test.ts | 6 +----- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c3db941fd..1e7f7c29b 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ See [action.yml](./action.yml) for more detail. | disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | No | | retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No | | special-characters-workaround | Uncommonly, some environments cannot tolerate special characters in a secret key. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. | No | -| use-existing-credentials | When set, the action will attempt to use existing credentials instead of fetching new credentials. Defaults to false. | No | +| use-existing-credentials | When set, the action will check if existing credentials are valid and exit if they are. Defaults to false. | No | #### Credential Lifetime The default session duration is **1 hour**. diff --git a/action.yml b/action.yml index 155a48209..91ba47548 100644 --- a/action.yml +++ b/action.yml @@ -74,7 +74,7 @@ inputs: description: Some environments do not support special characters in AWS_SECRET_ACCESS_KEY. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. This option is disabled by default required: false use-existing-credentials: - description: Set to true if you are using multiple workflows that use the same AWS Credentials. 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. + 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. outputs: aws-account-id: description: The AWS account ID for the provided credentials diff --git a/src/helpers.ts b/src/helpers.ts index fef371d78..2bc87d02a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -147,10 +147,13 @@ export function isDefined(i: T | undefined | null): i is T { export async function areCredentialsValid(credentialsClient: CredentialsClient) { const client = credentialsClient.stsClient; - const identity = await client.send(new GetCallerIdentityCommand({})); - const accountId = identity.Account; - if (!accountId) { + try { + const identity = await client.send(new GetCallerIdentityCommand({})); + if (identity.Account) { + return true; + } + return false; + } catch (_) { return false; } - return true; } diff --git a/src/index.ts b/src/index.ts index 01e8dce78..a35452bf4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,8 @@ export async function run() { const specialCharacterWorkaroundInput = core.getInput('special-characters-workaround', { required: false }) || 'false'; const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true'; - const useExistingCredentials = core.getInput('use-existing-credentials', { required: false }) || 'false'; + const useExistingCredentialsInput = core.getInput('use-existing-credentials', { required: false }) || 'false'; + const useExistingCredentials = useExistingCredentialsInput.toLowerCase() === 'true'; let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12; switch (true) { case specialCharacterWorkaround: @@ -119,13 +120,13 @@ export async function run() { let webIdentityToken: string; //if the user wants to attempt to use existing credentials, check if we have some already - if (useExistingCredentials === 'true') { + if (useExistingCredentials) { const validCredentials = await areCredentialsValid(credentialsClient); if (validCredentials) { - core.info('Pre-existing credentials are valid. No need to generate new ones.'); + core.notice('Pre-existing credentials are valid. No need to generate new ones.'); return; } - core.info('No valid credentials exist. Running as normal.'); + core.notice('No valid credentials exist. Running as normal.'); } // If OIDC is being used, generate token diff --git a/test/index.test.ts b/test/index.test.ts index 34e95687b..a95443da6 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -27,6 +27,7 @@ describe('Configure AWS Credentials', {}, () => { vi.spyOn(core, 'setOutput').mockImplementation((_n, _v) => {}); vi.spyOn(core, 'debug').mockImplementation((_m) => {}); vi.spyOn(core, 'info').mockImplementation((_m) => {}); + vi.spyOn(core, 'notice').mockImplementation((_m) => {}); // Remove any existing environment variables before each test to prevent the // SDK from picking them up process.env = { ...mocks.envs }; @@ -299,23 +300,17 @@ describe('Configure AWS Credentials', {}, () => { await run(); expect(core.setFailed).toHaveBeenCalled(); }); - it('re-uses existing credentials if they are valid', {}, async () => { - vi.clearAllMocks(); - mockedSTSClient.reset(); + it('gets new creds if told to reuse existing but they\'re invalid', {}, async () => { vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.USE_EXISTING_CREDENTIALS_INPUTS)); - vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - - mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS); - mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY_NO_CREDS }); + mockedSTSClient.on(GetCallerIdentityCommand).rejects(); await run(); - expect(core.info).toHaveBeenCalledWith('No valid credentials exist. Running as normal.') - - mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); - - mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS); + expect(core.notice).toHaveBeenCalledWith('No valid credentials exist. Running as normal.') + }); + it('doesn\'t get new creds if there are already valid ones and we said use them', {}, async () => { + vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.USE_EXISTING_CREDENTIALS_INPUTS)); + mockedSTSClient.on(GetCallerIdentityCommand).resolves(mocks.outputs.GET_CALLER_IDENTITY); await run(); - expect(core.info).toHaveBeenLastCalledWith('Pre-existing credentials are valid. No need to generate new ones.') + expect(core.setFailed).not.toHaveBeenCalled(); }) }); }); diff --git a/test/mockinputs.test.ts b/test/mockinputs.test.ts index f8ae6452b..c5908a818 100644 --- a/test/mockinputs.test.ts +++ b/test/mockinputs.test.ts @@ -28,10 +28,9 @@ const inputs = { 'aws-region': 'fake-region-1', }, USE_EXISTING_CREDENTIALS_INPUTS: { - 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', 'aws-region': 'fake-region-1', - 'special-characters-workaround': 'true', 'use-existing-credentials': 'true', + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', } }; @@ -61,9 +60,6 @@ const outputs = { GET_CALLER_IDENTITY: { Account: '111111111111', Arn: 'arn:aws:iam::111111111111:role/MY-ROLE', - }, - GET_CALLER_IDENTITY_NO_CREDS: { - }, FAKE_STS_ACCESS_KEY_ID: 'STSAWSACCESSKEYID', FAKE_STS_SECRET_ACCESS_KEY: 'STSAWSSECRETACCESSKEY',