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

implement last changes for vNext

This commit is contained in:
peterwoodworth 2023-07-03 16:13:19 -07:00
commit aa2675f083
No known key found for this signature in database
GPG key ID: 01931412FD685922
8 changed files with 858 additions and 677 deletions

View file

@ -107,6 +107,7 @@ overrides:
'@typescript-eslint/prefer-includes': [warn]
dot-notation: [off]
'@typescript-eslint/dot-notation': [error]
'@typescript-eslint/no-explicit-any': [off]
'@typescript-eslint/consistent-type-exports': [warn]
'@typescript-eslint/consistent-type-imports': [warn]
'@typescript-eslint/no-base-to-string': [error]

View file

@ -34,9 +34,6 @@ inputs:
description: The audience to use for the OIDC provider
required: false
default: sts.amazonaws.com
disable-oidc:
description: Strictly disable action from attempting to fetch credentials with OIDC
required: false
http-proxy:
description: 'Proxy to use for the AWS SDK agent'
required: false
@ -61,6 +58,18 @@ inputs:
managed-session-policies:
description: 'List of managed session policies'
required: false
output-credentials:
description: Whether to set credentials as step output
required: false
unset-current-credentials:
description: Whether to unset the existing credentials in your runner
required: false
disable-retry:
description: Whether to disable the retry and backoff mechanism when the assume role call fails
required: false
retry-max-attempts:
description: The maximum number of attempts it will attempt to retry the assume role call
required: false
outputs:
aws-account-id:
description: The AWS account ID for the provided credentials

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

@ -17583,14 +17583,14 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
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.exportCredentials = void 0;
exports.isDefined = exports.errorMessage = exports.retryAndBackoff = 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 client_sts_1 = __nccwpck_require__(2209);
const MAX_TAG_VALUE_LENGTH = 256;
const 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
function exportCredentials(creds) {
function exportCredentials(creds, outputCredentials) {
if (creds?.AccessKeyId) {
core.setSecret(creds.AccessKeyId);
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
@ -17607,8 +17607,27 @@ function exportCredentials(creds) {
// clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', '');
}
if (outputCredentials) {
if (creds?.AccessKeyId) {
core.setOutput('aws-access-key-id', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.setOutput('aws-secret-access-key', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.setOutput('aws-session-token', creds.SessionToken);
}
}
}
exports.exportCredentials = exportCredentials;
function unsetCredentials() {
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_REGION', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
}
exports.unsetCredentials = unsetCredentials;
function exportRegion(region) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
@ -17652,7 +17671,7 @@ function reset() {
}
exports.reset = reset;
// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time.
async function retryAndBackoff(fn, isRetryable, retries = 0, maxRetries = 12, base = 50) {
async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) {
try {
return await fn();
}
@ -17666,7 +17685,7 @@ async function retryAndBackoff(fn, isRetryable, retries = 0, maxRetries = 12, ba
if (retries === maxRetries) {
throw err;
}
return await retryAndBackoff(fn, isRetryable, retries, maxRetries, base);
return await retryAndBackoff(fn, isRetryable, maxRetries, retries, base);
}
}
exports.retryAndBackoff = retryAndBackoff;

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

@ -1,6 +1,7 @@
import type { Credentials } from '@aws-sdk/client-sts';
import type { CredentialsClient } from './CredentialsClient';
export declare function exportCredentials(creds?: Partial<Credentials>): void;
export declare function exportCredentials(creds?: Partial<Credentials>, outputCredentials?: boolean): void;
export declare function unsetCredentials(): void;
export declare function exportRegion(region: string): void;
export declare function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean): Promise<string>;
export declare function sanitizeGitHubVariables(name: string): string;
@ -8,7 +9,7 @@ export declare function defaultSleep(ms: number): Promise<unknown>;
declare let sleep: typeof defaultSleep;
export declare function withsleep(s: typeof sleep): void;
export declare function reset(): void;
export declare function retryAndBackoff<T>(fn: () => Promise<T>, isRetryable: boolean, retries?: number, maxRetries?: 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 isDefined<T>(i: T | undefined | null): i is T;
export {};

46
dist/index.js generated vendored
View file

@ -241,14 +241,14 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
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.exportCredentials = void 0;
exports.isDefined = exports.errorMessage = exports.retryAndBackoff = 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 client_sts_1 = __nccwpck_require__(2209);
const MAX_TAG_VALUE_LENGTH = 256;
const 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
function exportCredentials(creds) {
function exportCredentials(creds, outputCredentials) {
if (creds?.AccessKeyId) {
core.setSecret(creds.AccessKeyId);
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
@ -265,8 +265,27 @@ function exportCredentials(creds) {
// clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', '');
}
if (outputCredentials) {
if (creds?.AccessKeyId) {
core.setOutput('aws-access-key-id', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.setOutput('aws-secret-access-key', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.setOutput('aws-session-token', creds.SessionToken);
}
}
}
exports.exportCredentials = exportCredentials;
function unsetCredentials() {
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_REGION', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
}
exports.unsetCredentials = unsetCredentials;
function exportRegion(region) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
@ -310,7 +329,7 @@ function reset() {
}
exports.reset = reset;
// Retries the promise with exponential backoff if the error isRetryable up to maxRetries time.
async function retryAndBackoff(fn, isRetryable, retries = 0, maxRetries = 12, base = 50) {
async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, base = 50) {
try {
return await fn();
}
@ -324,7 +343,7 @@ async function retryAndBackoff(fn, isRetryable, retries = 0, maxRetries = 12, ba
if (retries === maxRetries) {
throw err;
}
return await retryAndBackoff(fn, isRetryable, retries, maxRetries, base);
return await retryAndBackoff(fn, isRetryable, maxRetries, retries, base);
}
}
exports.retryAndBackoff = retryAndBackoff;
@ -398,12 +417,18 @@ async function run() {
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const proxyServer = core.getInput('http-proxy', { required: false });
const disableOIDC = core.getInput('disable-oidc', { required: false });
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPoliciesInput = core.getMultilineInput('managed-session-policies', { required: false });
const managedSessionPolicies = [];
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
const outputCredentialsInput = core.getInput('output-credentials', { required: false }) || 'false';
const outputCredentials = outputCredentialsInput.toLowerCase() === 'true';
const unsetCurrentCredentialsInput = core.getInput('unset-current-credentials', { required: false }) || 'false';
const unsetCurrentCredentials = unsetCurrentCredentialsInput.toLowerCase() === 'true';
const disableRetryInput = core.getInput('disable-retry', { required: false }) || 'false';
const disableRetry = disableRetryInput.toLowerCase() === 'true';
const maxRetries = parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
for (const managedSessionPolicy of managedSessionPoliciesInput) {
managedSessionPolicies.push({ arn: managedSessionPolicy });
}
@ -412,11 +437,10 @@ async function run() {
// 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.
// So, we will log a warning when it is the only piece absent
if (!!roleToAssume &&
!webIdentityTokenFile &&
!AccessKeyId &&
!disableOIDC &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!roleChaining) {
core.info('It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?');
@ -425,9 +449,11 @@ async function run() {
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!AccessKeyId &&
!webIdentityTokenFile &&
!disableOIDC &&
!roleChaining);
};
if (unsetCurrentCredentials) {
(0, helpers_1.unsetCredentials)();
}
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
@ -477,9 +503,9 @@ async function run() {
inlineSessionPolicy,
managedSessionPolicies,
});
}, true);
}, !disableRetry, maxRetries);
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser.AssumedRoleId}`);
(0, helpers_1.exportCredentials)(roleCredentials.Credentials);
(0, helpers_1.exportCredentials)(roleCredentials.Credentials, outputCredentials);
// We need to validate the credentials in 2 of our use-cases
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// is set to `true` then we are NOT in a self-hosted runner.

View file

@ -8,7 +8,7 @@ const 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>) {
export function exportCredentials(creds?: Partial<Credentials>, outputCredentials?: boolean) {
if (creds?.AccessKeyId) {
core.setSecret(creds.AccessKeyId);
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
@ -26,6 +26,26 @@ export function exportCredentials(creds?: Partial<Credentials>) {
// clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', '');
}
if (outputCredentials) {
if (creds?.AccessKeyId) {
core.setOutput('aws-access-key-id', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.setOutput('aws-secret-access-key', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.setOutput('aws-session-token', creds.SessionToken);
}
}
}
export function unsetCredentials() {
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_REGION', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
}
export function exportRegion(region: string) {
@ -74,8 +94,8 @@ export function reset() {
export async function retryAndBackoff<T>(
fn: () => Promise<T>,
isRetryable: boolean,
retries = 0,
maxRetries = 12,
retries = 0,
base = 50
): Promise<T> {
try {
@ -90,7 +110,7 @@ export async function retryAndBackoff<T>(
if (retries === maxRetries) {
throw err;
}
return await retryAndBackoff(fn, isRetryable, retries, maxRetries, base);
return await retryAndBackoff(fn, isRetryable, maxRetries, retries, base);
}
}

View file

@ -1,7 +1,14 @@
import * as core from '@actions/core';
import { assumeRole } from './assumeRole';
import { CredentialsClient } from './CredentialsClient';
import { errorMessage, retryAndBackoff, exportRegion, exportCredentials, exportAccountId } from './helpers';
import {
errorMessage,
retryAndBackoff,
exportRegion,
exportCredentials,
exportAccountId,
unsetCredentials,
} from './helpers';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
@ -26,12 +33,18 @@ export async function run() {
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const proxyServer = core.getInput('http-proxy', { required: false });
const disableOIDC = core.getInput('disable-oidc', { required: false });
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPoliciesInput = core.getMultilineInput('managed-session-policies', { required: false });
const managedSessionPolicies: any[] = [];
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
const outputCredentialsInput = core.getInput('output-credentials', { required: false }) || 'false';
const outputCredentials = outputCredentialsInput.toLowerCase() === 'true';
const unsetCurrentCredentialsInput = core.getInput('unset-current-credentials', { required: false }) || 'false';
const unsetCurrentCredentials = unsetCurrentCredentialsInput.toLowerCase() === 'true';
const disableRetryInput = core.getInput('disable-retry', { required: false }) || 'false';
const disableRetry = disableRetryInput.toLowerCase() === 'true';
const maxRetries = parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
for (const managedSessionPolicy of managedSessionPoliciesInput) {
managedSessionPolicies.push({ arn: managedSessionPolicy });
}
@ -41,12 +54,11 @@ export async function run() {
// 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.
// So, we will log a warning when it is the only piece absent
if (
!!roleToAssume &&
!webIdentityTokenFile &&
!AccessKeyId &&
!disableOIDC &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!roleChaining
) {
@ -59,11 +71,14 @@ export async function run() {
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!AccessKeyId &&
!webIdentityTokenFile &&
!disableOIDC &&
!roleChaining
);
};
if (unsetCurrentCredentials) {
unsetCredentials();
}
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
@ -101,23 +116,27 @@ export async function run() {
// Get role credentials if configured to do so
if (roleToAssume) {
const roleCredentials = await retryAndBackoff(async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
});
}, true);
const roleCredentials = await retryAndBackoff(
async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
});
},
!disableRetry,
maxRetries
);
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser!.AssumedRoleId!}`);
exportCredentials(roleCredentials.Credentials);
exportCredentials(roleCredentials.Credentials, outputCredentials);
// We need to validate the credentials in 2 of our use-cases
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// is set to `true` then we are NOT in a self-hosted runner.

View file

@ -23,6 +23,10 @@ const FAKE_ACCOUNT_ID = '123456789012';
const FAKE_ROLE_ACCOUNT_ID = '111111111111';
const ROLE_NAME = 'MY-ROLE';
const ROLE_ARN = 'arn:aws:iam::111111111111:role/MY-ROLE';
const MANAGED_SESSION_POLICY_INPUT = [
'arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess',
'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess',
];
const ENVIRONMENT_VARIABLE_OVERRIDES = {
SHOW_STACK_TRACE: 'false',
GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME',
@ -54,6 +58,12 @@ function mockGetInput(requestResponse: Record<string, string>) {
};
}
function mockGetMultilineInput(requestResponse: Record<string, string[]>) {
return function (name: string, _options: unknown): string[] {
return requestResponse[name]!;
};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('fs', () => ({
...jest.requireActual('fs'),
@ -481,6 +491,40 @@ describe('Configure AWS Credentials', () => {
expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(12);
});
test('role assumption fails after one trial when disabling retry', async () => {
process.env['GITHUB_ACTIONS'] = 'true';
process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] = 'test-token';
jest
.spyOn(core, 'getInput')
.mockImplementation(
mockGetInput({ 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'disable-retry': 'true' })
);
mockedSTS.reset();
mockedSTS.on(AssumeRoleWithWebIdentityCommand).rejects();
await run();
expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(1);
});
test('max retries is configurable', async () => {
process.env['GITHUB_ACTIONS'] = 'true';
process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] = 'test-token';
jest.spyOn(core, 'getInput').mockImplementation(
mockGetInput({
'role-to-assume': ROLE_ARN,
'aws-region': FAKE_REGION,
'retry-max-attempts': '15',
})
);
mockedSTS.reset();
mockedSTS.on(AssumeRoleWithWebIdentityCommand).rejects();
await run();
expect(mockedSTS.commandCalls(AssumeRoleWithWebIdentityCommand).length).toEqual(15);
});
test('role external ID provided', async () => {
jest
.spyOn(core, 'getInput')
@ -614,4 +658,46 @@ describe('Configure AWS Credentials', () => {
expect(core.exportVariable).toReturn();
});
test('inline policy and managed session policies are provided in assume role calls', async () => {
jest
.spyOn(core, 'getInput')
.mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'inline-session-policy': 'inline' }));
jest
.spyOn(core, 'getMultilineInput')
.mockImplementation(mockGetMultilineInput({ 'managed-session-policies': MANAGED_SESSION_POLICY_INPUT }));
await run();
expect(mockedSTS.commandCalls(AssumeRoleCommand)[0]?.args[0].input).toEqual({
RoleArn: ROLE_ARN,
RoleSessionName: 'GitHubActions',
DurationSeconds: 3600,
Tags: [
{ Key: 'GitHub', Value: 'Actions' },
{ Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY },
{ Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW },
{ Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION },
{ Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED },
{ Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA },
{ Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF },
],
Policy: 'inline',
PolicyArns: [
{ arn: 'arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess' },
{ arn: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess' },
],
});
});
test('unsets credentials if enabled', async () => {
jest
.spyOn(core, 'getInput')
.mockImplementation(mockGetInput({ ...ASSUME_ROLE_INPUTS, 'unset-current-credentials': 'true' }));
await run();
expect(core.exportVariable).toHaveBeenCalledTimes(9);
});
});