From b72354411530485daa84834184103ba2bcd6ac78 Mon Sep 17 00:00:00 2001 From: peterwoodworth Date: Wed, 22 Mar 2023 15:08:39 -0700 Subject: [PATCH] fix: branch name is not sanitized, slight refactor --- src/assumeRole.ts | 8 ++-- src/helpers.ts | 119 +++++++++++++++++++++------------------------- src/index.ts | 13 ++--- 3 files changed, 64 insertions(+), 76 deletions(-) diff --git a/src/assumeRole.ts b/src/assumeRole.ts index 201a5ee..279fbf6 100644 --- a/src/assumeRole.ts +++ b/src/assumeRole.ts @@ -5,7 +5,7 @@ import * as core from '@actions/core'; import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts'; import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; import type { CredentialsClient } from './CredentialsClient'; -import { errorMessage, isDefined, sanitizeGithubActor, sanitizeGithubWorkflowName } from './helpers'; +import { errorMessage, isDefined, sanitizeGitHubVariables } from './helpers'; async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) { delete params.Tags; @@ -96,13 +96,13 @@ export async function assumeRole(params: assumeRoleParams) { const tagArray: Tag[] = [ { Key: 'GitHub', Value: 'Actions' }, { Key: 'Repository', Value: GITHUB_REPOSITORY }, - { Key: 'Workflow', Value: sanitizeGithubWorkflowName(GITHUB_WORKFLOW) }, + { Key: 'Workflow', Value: sanitizeGitHubVariables(GITHUB_WORKFLOW) }, { Key: 'Action', Value: GITHUB_ACTION }, - { Key: 'Actor', Value: sanitizeGithubActor(GITHUB_ACTOR) }, + { Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) }, { Key: 'Commit', Value: GITHUB_SHA }, ]; if (process.env['GITHUB_REF']) { - tagArray.push({ Key: 'Branch', Value: process.env['GITHUB_REF'] }); + tagArray.push({ Key: 'Branch', Value: sanitizeGitHubVariables(process.env['GITHUB_REF']) }); } const tags = roleSkipSessionTagging ? undefined : tagArray; if (!tags) { diff --git a/src/helpers.ts b/src/helpers.ts index 1cd519f..952c29a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -6,29 +6,56 @@ import type { CredentialsClient } from './CredentialsClient'; const MAX_TAG_VALUE_LENGTH = 256; const SANITIZATION_CHARACTER = '_'; -export function sanitizeGithubActor(actor: string) { - // In some circumstances the actor may contain square brackets. For example, if they're a bot ('[bot]') - // Square brackets are not allowed in AWS session tags - return actor.replace(/\[|\]/g, SANITIZATION_CHARACTER); +// Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets. +// Setting the credentials as secrets masks them in Github Actions logs +export function exportCredentials(creds?: Partial) { + if (creds?.AccessKeyId) { + core.setSecret(creds.AccessKeyId); + core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId); + } + + if (creds?.SecretAccessKey) { + core.setSecret(creds.SecretAccessKey); + core.exportVariable('AWS_SECRET_ACCESS_KEY', creds.SecretAccessKey); + } + + if (creds?.SessionToken) { + core.setSecret(creds.SessionToken); + core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken); + } else if (process.env['AWS_SESSION_TOKEN']) { + // clear session token from previous credentials action + core.exportVariable('AWS_SESSION_TOKEN', ''); + } } -export function sanitizeGithubWorkflowName(name: string) { - // Workflow names can be almost any valid UTF-8 string, but tags are more restrictive. - // This replaces anything not conforming to the tag restrictions by inverting the regular expression. - // See the AWS documentation for constraint specifics https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html. - const nameWithoutSpecialCharacters = name.replace(/[^\p{L}\p{Z}\p{N}_:/=+.-@-]/gu, SANITIZATION_CHARACTER); +export function exportRegion(region: string) { + core.exportVariable('AWS_DEFAULT_REGION', region); + core.exportVariable('AWS_REGION', region); +} + +// Obtains account ID from STS Client and sets it as output +export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: string) { + const client = credentialsClient.getStsClient(); + const identity = await client.send(new GetCallerIdentityCommand({})); + const accountId = identity.Account; + if (!accountId) { + throw new Error('Could not get Account ID from STS. Did you set credentials?'); + } + if (maskAccountId) { + core.setSecret(accountId); + } + core.setOutput('aws-account-id', accountId); + return accountId; +} + +// Tags have a more restrictive set of acceptable characters than GitHub environment variables can. +// This replaces anything not conforming to the tag restrictions by inverting the regular expression. +// See the AWS documentation for constraint specifics https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html. +export function sanitizeGitHubVariables(name: string) { + const nameWithoutSpecialCharacters = name.replace(/[^\p{L}\p{Z}\p{N}_.:/=+\-@]/gu, SANITIZATION_CHARACTER); const nameTruncated = nameWithoutSpecialCharacters.slice(0, MAX_TAG_VALUE_LENGTH); return nameTruncated; } -/* c8 ignore start */ -export function errorMessage(error: unknown) { - return error instanceof Error ? error.message : String(error); -} - -export function isDefined(i: T | undefined | null): i is T { - return i !== undefined && i !== null; -} -/* c8 ignore stop */ export async function defaultSleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -38,11 +65,12 @@ let sleep = defaultSleep; export function withsleep(s: typeof sleep) { sleep = s; } + export function reset() { sleep = defaultSleep; } -// retryAndBackoff retries with exponential backoff the promise if the error isRetryable upto maxRetries time. +// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time. export async function retryAndBackoff( fn: () => Promise, isRetryable: boolean, @@ -66,53 +94,12 @@ export async function retryAndBackoff( } } -export function exportCredentials(creds?: Partial) { - // Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets. - // Setting the credentials as secrets masks them in Github Actions logs - - // AWS_ACCESS_KEY_ID: - // Specifies an AWS access key associated with an IAM user or role - if (creds?.AccessKeyId) { - core.setSecret(creds.AccessKeyId); - core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId); - } - - // AWS_SECRET_ACCESS_KEY: - // Specifies the secret key associated with the access key. This is essentially the "password" for the access key. - if (creds?.SecretAccessKey) { - core.setSecret(creds.SecretAccessKey); - core.exportVariable('AWS_SECRET_ACCESS_KEY', creds.SecretAccessKey); - } - - // AWS_SESSION_TOKEN: - // Specifies the session token value that is required if you are using temporary security credentials. - if (creds?.SessionToken) { - core.setSecret(creds.SessionToken); - core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken); - } else if (process.env['AWS_SESSION_TOKEN']) { - // clear session token from previous credentials action - core.exportVariable('AWS_SESSION_TOKEN', ''); - } +/* c8 ignore start */ +export function errorMessage(error: unknown) { + return error instanceof Error ? error.message : String(error); } -export function exportRegion(region: string) { - // AWS_DEFAULT_REGION and AWS_REGION: - // Specifies the AWS Region to send requests to - core.exportVariable('AWS_DEFAULT_REGION', region); - core.exportVariable('AWS_REGION', region); -} - -export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: string) { - // Get the AWS account ID - const client = credentialsClient.getStsClient(); - const identity = await client.send(new GetCallerIdentityCommand({})); - const accountId = identity.Account; - if (!accountId) { - throw new Error('Could not get Account ID from STS. Did you set credentials?'); - } - if (maskAccountId) { - core.setSecret(accountId); - } - core.setOutput('aws-account-id', accountId); - return accountId; +export function isDefined(i: T | undefined | null): i is T { + return i !== undefined && i !== null; } +/* c8 ignore stop */ diff --git a/src/index.ts b/src/index.ts index 5f7b421..c1cca20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { assumeRole } from './assumeRole'; import { CredentialsClient } from './CredentialsClient'; import { errorMessage, retryAndBackoff, exportRegion, exportCredentials, exportAccountId } from './helpers'; -const DEFAULT_ROLE_DURATION = 3600; // One hour +const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds) const ROLE_SESSION_NAME = 'GitHubActions'; const REGION_REGEX = /^[a-z0-9-]+$/g; @@ -32,7 +32,7 @@ export async function run() { // 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, + // The `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variable is set when the `id-token` permission is granted. // This is necessary to authenticate with OIDC, but not strictly set just for OIDC. If it is not set and all other // checks pass, it is likely but not guaranteed that the user needs but lacks this permission in their workflow. // So, we will log a warning when it is the only piece absent, as well as add an opportunity to manually disable the entire check. @@ -80,14 +80,13 @@ export async function run() { exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }); } - // Attempt to load credentials from the GitHub OIDC provider. - // If a user provides an IAM Role Arn and DOESN'T provide an Access Key Id - // The only way to assume the role is via GitHub's OIDC provider. + // If OIDC is being used, generate token + // Else, validate that the SDK can pick up credentials let sourceAccountId: string; let webIdentityToken: string; if (useGitHubOIDCProvider()) { webIdentityToken = await core.getIDToken(audience); - // We don't validate the credentials here because we don't have them yet when using OIDC. + // Implement #359 } else { // Regardless of whether any source credentials were provided as inputs, // validate that the SDK can actually pick up credentials. This validates @@ -124,7 +123,9 @@ export async function run() { await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId); } await exportAccountId(credentialsClient, maskAccountId); + // implement #432 } else { + // implement #370 core.info('Proceeding with IAM user credentials'); } } catch (error) {