diff --git a/action.yml b/action.yml index 7e98591a9..f3cdc544c 100644 --- a/action.yml +++ b/action.yml @@ -40,6 +40,9 @@ inputs: mask-aws-account-id: description: Whether to mask the AWS account ID for these credentials as a secret value. By default the account ID will not be masked required: false + mask-arn: + description: Whether to mask the Amazon Resource Name (ARN) for these credentials as a secret value. By default the Amazon Resource Name (ARN) will not be masked + required: false role-duration-seconds: description: Role duration in seconds. Default is one hour. required: false diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index dd8b9ab7f..d0769e064 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -47665,17 +47665,31 @@ function exportRegion(region) { core.exportVariable('AWS_REGION', region); } // Obtains account ID from STS Client and sets it as output -async function exportAccountId(credentialsClient, maskAccountId) { +async function exportAccountId(credentialsClient, maskAccountId, maskArn) { const client = credentialsClient.stsClient; const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const accountId = identity.Account; + const arn = identity.Arn; if (!accountId) { throw new Error('Could not get Account ID from STS. Did you set credentials?'); } if (maskAccountId) { core.setSecret(accountId); } + else { + core.info(`Authenticated as accountId ${accountId}`); + } + if (!arn) { + throw new Error('Could not get Amazon Resource Name (ARN) from STS. Did you set credentials?'); + } + if (maskArn) { + core.setSecret(arn); + } + else { + core.info(`Authenticated as arn ${arn}`); + } core.setOutput('aws-account-id', accountId); + core.setOutput('arn', arn); return accountId; } // Tags have a more restrictive set of acceptable characters than GitHub environment variables can. diff --git a/dist/cleanup/src/helpers.d.ts b/dist/cleanup/src/helpers.d.ts index 350f3ed45..6596602e5 100644 --- a/dist/cleanup/src/helpers.d.ts +++ b/dist/cleanup/src/helpers.d.ts @@ -3,7 +3,7 @@ import type { CredentialsClient } from './CredentialsClient'; export declare function exportCredentials(creds?: Partial, outputCredentials?: boolean): void; export declare function unsetCredentials(): void; export declare function exportRegion(region: string): void; -export declare function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean): Promise; +export declare function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean, maskArn?: boolean): Promise; export declare function sanitizeGitHubVariables(name: string): string; export declare function defaultSleep(ms: number): Promise; declare let sleep: typeof defaultSleep; diff --git a/dist/index.js b/dist/index.js index 6b49b086a..a2f1060d7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -301,17 +301,31 @@ function exportRegion(region) { core.exportVariable('AWS_REGION', region); } // Obtains account ID from STS Client and sets it as output -async function exportAccountId(credentialsClient, maskAccountId) { +async function exportAccountId(credentialsClient, maskAccountId, maskArn) { const client = credentialsClient.stsClient; const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const accountId = identity.Account; + const arn = identity.Arn; if (!accountId) { throw new Error('Could not get Account ID from STS. Did you set credentials?'); } if (maskAccountId) { core.setSecret(accountId); } + else { + core.info(`Authenticated as accountId ${accountId}`); + } + if (!arn) { + throw new Error('Could not get Amazon Resource Name (ARN) from STS. Did you set credentials?'); + } + if (maskArn) { + core.setSecret(arn); + } + else { + core.info(`Authenticated as arn ${arn}`); + } core.setOutput('aws-account-id', accountId); + core.setOutput('arn', arn); return accountId; } // Tags have a more restrictive set of acceptable characters than GitHub environment variables can. @@ -429,6 +443,8 @@ async function run() { const audience = core.getInput('audience', { required: false }); const maskAccountIdInput = core.getInput('mask-aws-account-id', { required: false }) || 'false'; const maskAccountId = maskAccountIdInput.toLowerCase() === 'true'; + const maskArnInput = core.getInput('mask-arn', { required: false }) || 'false'; + const maskArn = maskArnInput.toLowerCase() === 'true'; const roleExternalId = core.getInput('role-external-id', { required: false }); const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false }); const roleDuration = parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION; @@ -519,14 +535,14 @@ async function run() { else if (!webIdentityTokenFile && !roleChaining) { // Proceed only if credentials can be picked up await credentialsClient.validateCredentials(); - sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId); + sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId, maskArn); } if (AccessKeyId || roleChaining) { // Validate that the SDK can actually pick up credentials. // This validates cases where this action is using existing environment credentials, // and cases where the user intended to provide input credentials but the secrets inputs resolved to empty strings. await credentialsClient.validateCredentials(AccessKeyId, roleChaining); - sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId); + sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId, maskArn); } // Get role credentials if configured to do so if (roleToAssume) { @@ -559,7 +575,7 @@ async function run() { if (!process.env['GITHUB_ACTIONS'] || AccessKeyId) { await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId); } - await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId); + await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId, maskArn); } else { core.info('Proceeding with IAM user credentials'); diff --git a/src/helpers.ts b/src/helpers.ts index 08e19e73f..b493c5d18 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -55,17 +55,33 @@ export function exportRegion(region: string) { } // Obtains account ID from STS Client and sets it as output -export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean) { +export async function exportAccountId( + credentialsClient: CredentialsClient, + maskAccountId?: boolean, + maskArn?: boolean +) { const client = credentialsClient.stsClient; const identity = await client.send(new GetCallerIdentityCommand({})); const accountId = identity.Account; + const arn = identity.Arn; if (!accountId) { throw new Error('Could not get Account ID from STS. Did you set credentials?'); } if (maskAccountId) { core.setSecret(accountId); + } else { + core.info(`Authenticated as accountId ${accountId}`); + } + if (!arn) { + throw new Error('Could not get Amazon Resource Name (ARN) from STS. Did you set credentials?'); + } + if (maskArn) { + core.setSecret(arn); + } else { + core.info(`Authenticated as arn ${arn}`); } core.setOutput('aws-account-id', accountId); + core.setOutput('arn', arn); return accountId; } diff --git a/src/index.ts b/src/index.ts index da296c70c..0d7bd7c34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,8 @@ export async function run() { const audience = core.getInput('audience', { required: false }); const maskAccountIdInput = core.getInput('mask-aws-account-id', { required: false }) || 'false'; const maskAccountId = maskAccountIdInput.toLowerCase() === 'true'; + const maskArnInput = core.getInput('mask-arn', { required: false }) || 'false'; + const maskArn = maskArnInput.toLowerCase() === 'true'; const roleExternalId = core.getInput('role-external-id', { required: false }); const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false }); const roleDuration = parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION; @@ -131,7 +133,7 @@ export async function run() { } else if (!webIdentityTokenFile && !roleChaining) { // Proceed only if credentials can be picked up await credentialsClient.validateCredentials(); - sourceAccountId = await exportAccountId(credentialsClient, maskAccountId); + sourceAccountId = await exportAccountId(credentialsClient, maskAccountId, maskArn); } if (AccessKeyId || roleChaining) { @@ -139,7 +141,7 @@ export async function run() { // This validates cases where this action is using existing environment credentials, // and cases where the user intended to provide input credentials but the secrets inputs resolved to empty strings. await credentialsClient.validateCredentials(AccessKeyId, roleChaining); - sourceAccountId = await exportAccountId(credentialsClient, maskAccountId); + sourceAccountId = await exportAccountId(credentialsClient, maskAccountId, maskArn); } // Get role credentials if configured to do so @@ -177,7 +179,7 @@ export async function run() { if (!process.env['GITHUB_ACTIONS'] || AccessKeyId) { await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId); } - await exportAccountId(credentialsClient, maskAccountId); + await exportAccountId(credentialsClient, maskAccountId, maskArn); } else { core.info('Proceeding with IAM user credentials'); } diff --git a/test/index.test.ts b/test/index.test.ts index dd0658977..7063f3d59 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -20,6 +20,7 @@ const FAKE_STS_SESSION_TOKEN = 'STSAWSSESSIONTOKEN'; const FAKE_ASSUMED_ROLE_ID = 'AROAFAKEASSUMEDROLEID'; const FAKE_REGION = 'fake-region-1'; const FAKE_ACCOUNT_ID = '123456789012'; +const FAKE_USER_ARN = 'arn:aws:iam:123456789012:user/FAKE-USER'; const FAKE_ROLE_ACCOUNT_ID = '111111111111'; const ROLE_NAME = 'MY-ROLE'; const ROLE_ARN = 'arn:aws:iam::111111111111:role/MY-ROLE'; @@ -103,7 +104,10 @@ describe('Configure AWS Credentials', () => { }); mockedSTS .on(GetCallerIdentityCommand) - .resolvesOnce({ Account: FAKE_ACCOUNT_ID }) + .resolvesOnce({ + Account: FAKE_ACCOUNT_ID, + Arn: FAKE_USER_ARN, + }) .resolvesOnce({ Account: FAKE_ROLE_ACCOUNT_ID }); mockedSTS.on(AssumeRoleCommand).resolves({ Credentials: { @@ -156,6 +160,9 @@ describe('Configure AWS Credentials', () => { expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', FAKE_REGION); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', FAKE_REGION); expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.info).toHaveBeenCalledWith(`Authenticated as accountId ${FAKE_ACCOUNT_ID}`); + expect(core.setOutput).toHaveBeenCalledWith('arn', FAKE_USER_ARN); + expect(core.info).toHaveBeenCalledWith(`Authenticated as arn ${FAKE_USER_ARN}`); }); test('action fails when github env vars are not set', async () => { @@ -234,6 +241,9 @@ describe('Configure AWS Credentials', () => { expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'eu-west-1'); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'eu-west-1'); expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.info).toHaveBeenCalledWith(`Authenticated as accountId ${FAKE_ACCOUNT_ID}`); + expect(core.setOutput).toHaveBeenCalledWith('arn', FAKE_USER_ARN); + expect(core.info).toHaveBeenCalledWith(`Authenticated as arn ${FAKE_USER_ARN}`); }); test('existing env var creds are cleared', async () => { @@ -256,6 +266,9 @@ describe('Configure AWS Credentials', () => { expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'eu-west-1'); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'eu-west-1'); expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.info).toHaveBeenCalledWith(`Authenticated as accountId ${FAKE_ACCOUNT_ID}`); + expect(core.setOutput).toHaveBeenCalledWith('arn', FAKE_USER_ARN); + expect(core.info).toHaveBeenCalledWith(`Authenticated as arn ${FAKE_USER_ARN}`); }); test('validates region name', async () => { @@ -295,10 +308,33 @@ describe('Configure AWS Credentials', () => { expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'us-east-1'); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'us-east-1'); expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.setOutput).toHaveBeenCalledWith('arn', FAKE_USER_ARN); + expect(core.info).toHaveBeenCalledWith(`Authenticated as arn ${FAKE_USER_ARN}`); expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID); expect(core.setSecret).toHaveBeenCalledTimes(3); }); + test('can opt into masking ARN', async () => { + const mockInputs = { ...CREDS_INPUTS, 'aws-region': 'us-east-1', 'mask-arn': 'true' }; + jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(mockInputs)); + + await run(); + + expect(mockedSTS.commandCalls(AssumeRoleCommand)).toHaveLength(0); + expect(core.exportVariable).toHaveBeenCalledTimes(4); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', FAKE_ACCESS_KEY_ID); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCESS_KEY_ID); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', FAKE_SECRET_ACCESS_KEY); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_SECRET_ACCESS_KEY); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'us-east-1'); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'us-east-1'); + expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID); + expect(core.info).toHaveBeenCalledWith(`Authenticated as accountId ${FAKE_ACCOUNT_ID}`); + expect(core.setOutput).toHaveBeenCalledWith('arn', FAKE_USER_ARN); + expect(core.setSecret).toHaveBeenCalledWith(FAKE_USER_ARN); + expect(core.setSecret).toHaveBeenCalledTimes(3); + }); + test('error is caught by core.setFailed and caught', async () => { jest.spyOn(core, 'getInput').mockImplementation(mockGetInput(DEFAULT_INPUTS)); mockedSTS.reset(); @@ -851,6 +887,6 @@ describe('Configure AWS Credentials', () => { await run(); - expect(core.setOutput).toHaveBeenCalledTimes(4); + expect(core.setOutput).toHaveBeenCalledTimes(5); }); });