1
0
Fork 0
mirror of synced 2026-06-05 14:58:19 +00:00

feat: getIDToken retry, feat: special character in key retry

This commit is contained in:
peterwoodworth 2023-07-05 18:55:04 -07:00
commit 7b893ba14b
No known key found for this signature in database
GPG key ID: 01931412FD685922
10 changed files with 200 additions and 35 deletions

View file

@ -1,5 +1,4 @@
## WIP name: Run tests
name: Devel_workflow
on: on:
workflow_dispatch: workflow_dispatch:

View file

@ -1,16 +0,0 @@
on:
[pull_request]
name: Run Unit Tests
jobs:
test:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run tests
run: |
npm ci
npm run test

26
.github/workflows/unit-tests.yml vendored Normal file
View file

@ -0,0 +1,26 @@
on:
[pull_request]
name: Run unit tests
jobs:
unit-test:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
node: [14, 16, 18]
name: Run unit tests
runs-on: ${{ matrix.os }}
timeout-minutes: 5
steps:
- name: "Checkout repository"
uses: actions/checkout@v3
- name: "Setup node"
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- name: "Install dependencies"
uses: bahmutov/npm-install@v1
- name: "Run tests"
run: npm run test --if-present

19
dist/cleanup/index.js generated vendored
View file

@ -17583,11 +17583,12 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isDefined = exports.errorMessage = exports.retryAndBackoff = exports.reset = exports.withsleep = exports.defaultSleep = exports.sanitizeGitHubVariables = exports.exportAccountId = exports.exportRegion = exports.unsetCredentials = exports.exportCredentials = void 0; exports.isDefined = exports.errorMessage = exports.retryAndBackoff = exports.verifyKeys = exports.reset = exports.withsleep = exports.defaultSleep = exports.sanitizeGitHubVariables = exports.exportAccountId = exports.exportRegion = exports.unsetCredentials = exports.exportCredentials = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const client_sts_1 = __nccwpck_require__(2209); const client_sts_1 = __nccwpck_require__(2209);
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_'; const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
// Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets. // 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 // Setting the credentials as secrets masks them in Github Actions logs
function exportCredentials(creds, outputCredentials) { function exportCredentials(creds, outputCredentials) {
@ -17670,6 +17671,22 @@ function reset() {
sleep = defaultSleep; sleep = defaultSleep;
} }
exports.reset = reset; exports.reset = reset;
function verifyKeys(creds) {
if (!creds) {
return;
}
if (creds.AccessKeyId) {
if (SPECIAL_CHARS_REGEX.test(creds.AccessKeyId)) {
throw new Error('AccessKeyId contains special characters.');
}
}
if (creds.SecretAccessKey) {
if (SPECIAL_CHARS_REGEX.test(creds.SecretAccessKey)) {
throw new Error('SecretAccessKey contains special characters.');
}
}
}
exports.verifyKeys = verifyKeys;
// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time. // Retries the promise with exponential backoff if the error isRetryable up to maxRetries time.
async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) { async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) {
try { try {

1
dist/cleanup/src/helpers.d.ts generated vendored
View file

@ -9,6 +9,7 @@ export declare function defaultSleep(ms: number): Promise<unknown>;
declare let sleep: typeof defaultSleep; declare let sleep: typeof defaultSleep;
export declare function withsleep(s: typeof sleep): void; export declare function withsleep(s: typeof sleep): void;
export declare function reset(): void; export declare function reset(): void;
export declare function verifyKeys(creds: Partial<Credentials> | undefined): void;
export declare function retryAndBackoff<T>(fn: () => Promise<T>, isRetryable: boolean, maxRetries?: number, retries?: number, base?: number): Promise<T>; export declare function retryAndBackoff<T>(fn: () => Promise<T>, isRetryable: boolean, maxRetries?: number, retries?: number, base?: number): Promise<T>;
export declare function errorMessage(error: unknown): string; export declare function errorMessage(error: unknown): string;
export declare function isDefined<T>(i: T | undefined | null): i is T; export declare function isDefined<T>(i: T | undefined | null): i is T;

40
dist/index.js generated vendored
View file

@ -109,10 +109,12 @@ async function assumeRoleWithOIDC(params, client, webIdentityToken) {
delete params.Tags; delete params.Tags;
core.info('Assuming role with OIDC'); core.info('Assuming role with OIDC');
try { try {
return await client.send(new client_sts_1.AssumeRoleWithWebIdentityCommand({ const creds = await client.send(new client_sts_1.AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
})); }));
(0, helpers_1.verifyKeys)(creds.Credentials);
return creds;
} }
catch (error) { catch (error) {
throw new Error(`Could not assume role with OIDC: ${(0, helpers_1.errorMessage)(error)}`); throw new Error(`Could not assume role with OIDC: ${(0, helpers_1.errorMessage)(error)}`);
@ -130,10 +132,12 @@ async function assumeRoleWithWebIdentityTokenFile(params, client, webIdentityTok
try { try {
const webIdentityToken = fs_1.default.readFileSync(webIdentityTokenFilePath, 'utf8'); const webIdentityToken = fs_1.default.readFileSync(webIdentityTokenFilePath, 'utf8');
delete params.Tags; delete params.Tags;
return await client.send(new client_sts_1.AssumeRoleWithWebIdentityCommand({ const creds = await client.send(new client_sts_1.AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
})); }));
(0, helpers_1.verifyKeys)(creds.Credentials);
return creds;
} }
catch (error) { catch (error) {
throw new Error(`Could not assume role with web identity token file: ${(0, helpers_1.errorMessage)(error)}`); throw new Error(`Could not assume role with web identity token file: ${(0, helpers_1.errorMessage)(error)}`);
@ -142,7 +146,9 @@ async function assumeRoleWithWebIdentityTokenFile(params, client, webIdentityTok
async function assumeRoleWithCredentials(params, client) { async function assumeRoleWithCredentials(params, client) {
core.info('Assuming role with user credentials'); core.info('Assuming role with user credentials');
try { try {
return await client.send(new client_sts_1.AssumeRoleCommand({ ...params })); const creds = await client.send(new client_sts_1.AssumeRoleCommand({ ...params }));
(0, helpers_1.verifyKeys)(creds.Credentials);
return creds;
} }
catch (error) { catch (error) {
throw new Error(`Could not assume role with user credentials: ${(0, helpers_1.errorMessage)(error)}`); throw new Error(`Could not assume role with user credentials: ${(0, helpers_1.errorMessage)(error)}`);
@ -241,11 +247,12 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isDefined = exports.errorMessage = exports.retryAndBackoff = exports.reset = exports.withsleep = exports.defaultSleep = exports.sanitizeGitHubVariables = exports.exportAccountId = exports.exportRegion = exports.unsetCredentials = exports.exportCredentials = void 0; exports.isDefined = exports.errorMessage = exports.retryAndBackoff = exports.verifyKeys = exports.reset = exports.withsleep = exports.defaultSleep = exports.sanitizeGitHubVariables = exports.exportAccountId = exports.exportRegion = exports.unsetCredentials = exports.exportCredentials = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const client_sts_1 = __nccwpck_require__(2209); const client_sts_1 = __nccwpck_require__(2209);
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_'; const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
// Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets. // 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 // Setting the credentials as secrets masks them in Github Actions logs
function exportCredentials(creds, outputCredentials) { function exportCredentials(creds, outputCredentials) {
@ -328,6 +335,22 @@ function reset() {
sleep = defaultSleep; sleep = defaultSleep;
} }
exports.reset = reset; exports.reset = reset;
function verifyKeys(creds) {
if (!creds) {
return;
}
if (creds.AccessKeyId) {
if (SPECIAL_CHARS_REGEX.test(creds.AccessKeyId)) {
throw new Error('AccessKeyId contains special characters.');
}
}
if (creds.SecretAccessKey) {
if (SPECIAL_CHARS_REGEX.test(creds.SecretAccessKey)) {
throw new Error('SecretAccessKey contains special characters.');
}
}
}
exports.verifyKeys = verifyKeys;
// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time. // Retries the promise with exponential backoff if the error isRetryable up to maxRetries time.
async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) { async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) {
try { try {
@ -465,7 +488,14 @@ async function run() {
// If OIDC is being used, generate token // If OIDC is being used, generate token
// Else, validate that the SDK can pick up credentials // Else, validate that the SDK can pick up credentials
if (useGitHubOIDCProvider()) { if (useGitHubOIDCProvider()) {
webIdentityToken = await core.getIDToken(audience); try {
webIdentityToken = await (0, helpers_1.retryAndBackoff)(async () => {
return core.getIDToken(audience);
}, !disableRetry, maxRetries);
}
catch (error) {
throw new Error(`getIDToken call failed: ${(0, helpers_1.errorMessage)(error)}`);
}
} }
else if (AccessKeyId) { else if (AccessKeyId) {
if (!SecretAccessKey) { if (!SecretAccessKey) {

View file

@ -5,18 +5,20 @@ import * as core from '@actions/core';
import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts'; import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts';
import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
import type { CredentialsClient } from './CredentialsClient'; import type { CredentialsClient } from './CredentialsClient';
import { errorMessage, isDefined, sanitizeGitHubVariables } from './helpers'; import { errorMessage, isDefined, sanitizeGitHubVariables, verifyKeys } from './helpers';
async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) { async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) {
delete params.Tags; delete params.Tags;
core.info('Assuming role with OIDC'); core.info('Assuming role with OIDC');
try { try {
return await client.send( const creds = await client.send(
new AssumeRoleWithWebIdentityCommand({ new AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
}) })
); );
verifyKeys(creds.Credentials);
return creds;
} catch (error) { } catch (error) {
throw new Error(`Could not assume role with OIDC: ${errorMessage(error)}`); throw new Error(`Could not assume role with OIDC: ${errorMessage(error)}`);
} }
@ -41,12 +43,14 @@ async function assumeRoleWithWebIdentityTokenFile(
try { try {
const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8'); const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8');
delete params.Tags; delete params.Tags;
return await client.send( const creds = await client.send(
new AssumeRoleWithWebIdentityCommand({ new AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
}) })
); );
verifyKeys(creds.Credentials);
return creds;
} catch (error) { } catch (error) {
throw new Error(`Could not assume role with web identity token file: ${errorMessage(error)}`); throw new Error(`Could not assume role with web identity token file: ${errorMessage(error)}`);
} }
@ -55,7 +59,9 @@ async function assumeRoleWithWebIdentityTokenFile(
async function assumeRoleWithCredentials(params: AssumeRoleCommandInput, client: STSClient) { async function assumeRoleWithCredentials(params: AssumeRoleCommandInput, client: STSClient) {
core.info('Assuming role with user credentials'); core.info('Assuming role with user credentials');
try { try {
return await client.send(new AssumeRoleCommand({ ...params })); const creds = await client.send(new AssumeRoleCommand({ ...params }));
verifyKeys(creds.Credentials);
return creds;
} catch (error) { } catch (error) {
throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`); throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`);
} }

View file

@ -5,6 +5,7 @@ import type { CredentialsClient } from './CredentialsClient';
const MAX_TAG_VALUE_LENGTH = 256; const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_'; const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
// Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets. // 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 // Setting the credentials as secrets masks them in Github Actions logs
@ -90,6 +91,22 @@ export function reset() {
sleep = defaultSleep; sleep = defaultSleep;
} }
export function verifyKeys(creds: Partial<Credentials> | undefined) {
if (!creds) {
return;
}
if (creds.AccessKeyId) {
if (SPECIAL_CHARS_REGEX.test(creds.AccessKeyId)) {
throw new Error('AccessKeyId contains special characters.');
}
}
if (creds.SecretAccessKey) {
if (SPECIAL_CHARS_REGEX.test(creds.SecretAccessKey)) {
throw new Error('SecretAccessKey contains special characters.');
}
}
}
// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time. // Retries the promise with exponential backoff if the error isRetryable up to maxRetries time.
export async function retryAndBackoff<T>( export async function retryAndBackoff<T>(
fn: () => Promise<T>, fn: () => Promise<T>,

View file

@ -92,7 +92,17 @@ export async function run() {
// If OIDC is being used, generate token // If OIDC is being used, generate token
// Else, validate that the SDK can pick up credentials // Else, validate that the SDK can pick up credentials
if (useGitHubOIDCProvider()) { if (useGitHubOIDCProvider()) {
webIdentityToken = await core.getIDToken(audience); try {
webIdentityToken = await retryAndBackoff(
async () => {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries
);
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
} else if (AccessKeyId) { } else if (AccessKeyId) {
if (!SecretAccessKey) { if (!SecretAccessKey) {
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided"); throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");

View file

@ -12,12 +12,12 @@ import { withsleep, reset } from '../src/helpers';
import { run } from '../src/index'; import { run } from '../src/index';
// #region // #region
const FAKE_ACCESS_KEY_ID = 'MY-AWS-ACCESS-KEY-ID'; const FAKE_ACCESS_KEY_ID = 'MYAWSACCESSKEYID';
const FAKE_SECRET_ACCESS_KEY = 'MY-AWS-SECRET-ACCESS-KEY'; const FAKE_SECRET_ACCESS_KEY = 'MYAWSSECRETACCESSKEY';
const FAKE_SESSION_TOKEN = 'MY-AWS-SESSION-TOKEN'; const FAKE_SESSION_TOKEN = 'MYAWSSESSIONTOKEN';
const FAKE_STS_ACCESS_KEY_ID = 'STS-AWS-ACCESS-KEY-ID'; const FAKE_STS_ACCESS_KEY_ID = 'STSAWSACCESSKEYID';
const FAKE_STS_SECRET_ACCESS_KEY = 'STS-AWS-SECRET-ACCESS-KEY'; const FAKE_STS_SECRET_ACCESS_KEY = 'STSAWSSECRETACCESSKEY';
const FAKE_STS_SESSION_TOKEN = 'STS-AWS-SESSION-TOKEN'; const FAKE_STS_SESSION_TOKEN = 'STSAWSSESSIONTOKEN';
const FAKE_REGION = 'fake-region-1'; const FAKE_REGION = 'fake-region-1';
const FAKE_ACCOUNT_ID = '123456789012'; const FAKE_ACCOUNT_ID = '123456789012';
const FAKE_ROLE_ACCOUNT_ID = '111111111111'; const FAKE_ROLE_ACCOUNT_ID = '111111111111';
@ -453,6 +453,24 @@ describe('Configure AWS Credentials', () => {
DurationSeconds: 3600, DurationSeconds: 3600,
WebIdentityToken: 'testtoken', WebIdentityToken: 'testtoken',
}); });
expect(core.getIDToken).toHaveBeenCalledTimes(1);
});
test('getIDToken call retries when failing', async () => {
process.env['GITHUB_ACTIONS'] = 'true';
process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] = 'test-token';
jest.spyOn(core, 'getIDToken').mockImplementation(() => {
throw new Error('test error');
});
jest
.spyOn(core, 'getInput')
.mockImplementation(mockGetInput({ 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION }));
await run();
expect(core.getIDToken).toHaveBeenCalledTimes(12);
expect(core.setFailed).toHaveBeenCalledWith('getIDToken call failed: test error');
}); });
test('GH OIDC With custom role duration', async () => { test('GH OIDC With custom role duration', async () => {
@ -507,6 +525,63 @@ describe('Configure AWS Credentials', () => {
expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(1); expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(1);
}); });
test('role assumption fails if access key id contains special characters', async () => {
jest.spyOn(core, 'getInput').mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS }));
mockedSTS.on(AssumeRoleCommand).resolves({
Credentials: {
AccessKeyId: 'asdf+',
SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY,
SessionToken: FAKE_STS_SESSION_TOKEN,
Expiration: new Date(8640000000000000),
},
});
await run();
expect(mockedSTS.commandCalls(AssumeRoleCommand).length).toEqual(12);
expect(core.setFailed).toHaveBeenCalledWith(
'Could not assume role with user credentials: AccessKeyId contains special characters.'
);
});
test('role assumption fails if secret access key contains special characters', async () => {
jest.spyOn(core, 'getInput').mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS }));
mockedSTS.on(AssumeRoleCommand).resolves({
Credentials: {
AccessKeyId: FAKE_STS_ACCESS_KEY_ID,
SecretAccessKey: 'asdf+',
SessionToken: FAKE_STS_SESSION_TOKEN,
Expiration: new Date(8640000000000000),
},
});
await run();
expect(mockedSTS.commandCalls(AssumeRoleCommand).length).toEqual(12);
expect(core.setFailed).toHaveBeenCalledWith(
'Could not assume role with user credentials: SecretAccessKey contains special characters.'
);
});
test('role assumption succeeds if keys have no special characters', async () => {
jest.spyOn(core, 'getInput').mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS }));
mockedSTS.on(AssumeRoleCommand).resolves({
Credentials: {
AccessKeyId: FAKE_STS_ACCESS_KEY_ID,
SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY,
SessionToken: FAKE_STS_SESSION_TOKEN,
Expiration: new Date(8640000000000000),
},
});
await run();
expect(mockedSTS.commandCalls(AssumeRoleCommand).length).toEqual(1);
});
test('max retries is configurable', async () => { test('max retries is configurable', async () => {
process.env['GITHUB_ACTIONS'] = 'true'; process.env['GITHUB_ACTIONS'] = 'true';
process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] = 'test-token'; process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] = 'test-token';