fix: branch name is not sanitized, slight refactor
This commit is contained in:
parent
12d07d4800
commit
b723544115
3 changed files with 63 additions and 75 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
115
src/helpers.ts
115
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<Credentials>) {
|
||||
if (creds?.AccessKeyId) {
|
||||
core.setSecret(creds.AccessKeyId);
|
||||
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
|
||||
}
|
||||
|
||||
export function sanitizeGithubWorkflowName(name: string) {
|
||||
// Workflow names can be almost any valid UTF-8 string, but tags are more restrictive.
|
||||
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 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.
|
||||
const nameWithoutSpecialCharacters = name.replace(/[^\p{L}\p{Z}\p{N}_:/=+.-@-]/gu, SANITIZATION_CHARACTER);
|
||||
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<T>(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<T>(
|
||||
fn: () => Promise<T>,
|
||||
isRetryable: boolean,
|
||||
|
|
@ -66,53 +94,12 @@ export async function retryAndBackoff<T>(
|
|||
}
|
||||
}
|
||||
|
||||
export function exportCredentials(creds?: Partial<Credentials>) {
|
||||
// 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);
|
||||
/* c8 ignore start */
|
||||
export function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
// 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', '');
|
||||
}
|
||||
}
|
||||
|
||||
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<T>(i: T | undefined | null): i is T {
|
||||
return i !== undefined && i !== null;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
|
|
|||
13
src/index.ts
13
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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue