feat!: initial v6 work

This commit is contained in:
kellertk
2025-10-08 13:33:21 -07:00
parent f226b0540e
commit d179403b3e
7 changed files with 659 additions and 537 deletions

View File

@@ -12,10 +12,6 @@
"performance": {
"noDelete": "off"
},
"correctness": {
// Specifying a radix disables interpretation of 0x as a number (needed for backwards compat)
"useParseIntRadix": "off"
},
"complexity": {
"noExtraBooleanCast": "off"
}

View File

@@ -1,104 +1,96 @@
import { info } from '@actions/core';
import { STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { ProxyAgent } from 'proxy-agent';
import { errorMessage, getCallerIdentity } from './helpers';
import { ProxyResolver } from './ProxyResolver';
import {
fromContainerMetadata,
fromEnv,
fromInstanceMetadata,
fromNodeProviderChain,
fromTemporaryCredentials,
fromWebToken,
} from '@aws-sdk/credential-providers';
import type { AwsCredentialIdentityProvider } from '@aws-sdk/types';
import type { NodeHttpHandler } from '@smithy/node-http-handler';
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
type EnvMode = { mode: 'env' };
type WebIdentityMode = { mode: 'web-identity'; webIdentityToken: string } & AssumeRoleOptions;
type StsMode = { mode: 'sts'; masterCredentials?: AwsCredentialIdentityProvider } & AssumeRoleOptions;
type ContainerMetadataMode = { mode: 'container-metadata' };
type InstanceMetadataMode = { mode: 'instance-metadata' };
export interface CredentialsClientProps {
region?: string;
proxyServer?: string;
noProxy?: string;
}
type AssumeRoleOptions = {
roleArn: string;
roleSessionName?: string;
durationSeconds?: number;
policy?: string;
policyArns?: { arn: string }[];
region: string;
requestHandler?: NodeHttpHandler;
};
export type CredentialsClientProps = EnvMode | WebIdentityMode | StsMode | ContainerMetadataMode | InstanceMetadataMode;
export class CredentialsClient {
public region?: string;
private _stsClient?: STSClient;
private readonly requestHandler?: NodeHttpHandler;
readonly DEFAULT_ROLE_DURATION = 3600;
readonly DEFAULT_ROLE_SESSION_NAME = 'GitHubActions';
private provider: AwsCredentialIdentityProvider;
constructor(props: CredentialsClientProps) {
if (props.region !== undefined) {
this.region = props.region;
}
if (props.proxyServer) {
info('Configuring proxy handler for STS client');
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
httpProxy: props.proxyServer,
httpsProxy: props.proxyServer,
};
if (props.noProxy !== undefined) {
proxyOptions.noProxy = props.noProxy;
}
const getProxyForUrl = new ProxyResolver(proxyOptions).getProxyForUrl;
const handler = new ProxyAgent({ getProxyForUrl });
this.requestHandler = new NodeHttpHandler({
httpsAgent: handler,
httpAgent: handler,
});
}
this.provider = this.createCredentialChain(props);
}
public get stsClient(): STSClient {
if (!this._stsClient) {
const config = { customUserAgent: USER_AGENT } as {
customUserAgent: string;
region?: string;
requestHandler?: NodeHttpHandler;
};
if (this.region !== undefined) config.region = this.region;
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
this._stsClient = new STSClient(config);
}
return this._stsClient;
}
private createCredentialChain(props: CredentialsClientProps): AwsCredentialIdentityProvider {
const primaryProvider = this.getPrimaryProvider(props);
const fallbackProvider = fromNodeProviderChain();
public async validateCredentials(
expectedAccessKeyId?: string,
roleChaining?: boolean,
expectedAccountIds?: string[],
) {
let credentials: AwsCredentialIdentity;
try {
credentials = await this.loadCredentials();
if (!credentials.accessKeyId) {
throw new Error('Access key ID empty after loading credentials');
}
} catch (error) {
throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`);
}
if (expectedAccountIds && expectedAccountIds.length > 0 && expectedAccountIds[0] !== '') {
let callerIdentity: Awaited<ReturnType<typeof getCallerIdentity>>;
return async () => {
try {
callerIdentity = await getCallerIdentity(this.stsClient);
} catch (error) {
throw new Error(`Could not validate account ID of credentials: ${errorMessage(error)}`);
return await primaryProvider();
} catch {
return await fallbackProvider();
}
if (!callerIdentity.Account || !expectedAccountIds.includes(callerIdentity.Account)) {
throw new Error(
`The account ID of the provided credentials (${
callerIdentity.Account ?? 'unknown'
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
);
}
}
};
}
if (!roleChaining) {
const actualAccessKeyId = credentials.accessKeyId;
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
throw new Error(
'Credentials loaded by the SDK do not match the expected access key ID configured by the action',
);
}
private getPrimaryProvider(props: CredentialsClientProps): AwsCredentialIdentityProvider {
switch (props.mode) {
case 'env':
return fromEnv();
case 'web-identity':
return fromWebToken({
clientConfig: {
region: props.region,
...(props.requestHandler && { requestHandler: props.requestHandler }),
maxAttempts: 1, // Disable built-in retry logic
},
roleArn: props.roleArn,
webIdentityToken: props.webIdentityToken,
durationSeconds: props.durationSeconds ?? this.DEFAULT_ROLE_DURATION,
roleSessionName: props.roleSessionName ?? this.DEFAULT_ROLE_SESSION_NAME,
...(props.policy && { policy: props.policy }),
...(props.policyArns && { policyArns: props.policyArns }),
});
case 'sts':
return fromTemporaryCredentials({
params: {
RoleArn: props.roleArn,
DurationSeconds: props.durationSeconds ?? this.DEFAULT_ROLE_DURATION,
RoleSessionName: props.roleSessionName ?? this.DEFAULT_ROLE_SESSION_NAME,
...(props.policy && { Policy: props.policy }),
...(props.policyArns && { PolicyArns: props.policyArns }),
},
clientConfig: {
region: props.region,
...(props.requestHandler && { requestHandler: props.requestHandler }),
maxAttempts: 1, // Disable built-in retry logic
},
...(props.masterCredentials && { masterCredentials: props.masterCredentials }),
});
case 'container-metadata':
return fromContainerMetadata();
case 'instance-metadata':
return fromInstanceMetadata();
}
}
private async loadCredentials() {
const config = {} as { requestHandler?: NodeHttpHandler };
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
const client = new STSClient(config);
return client.config.credentials();
getCredentials(): AwsCredentialIdentityProvider {
return this.provider;
}
}

View File

@@ -33,7 +33,7 @@ export class ProxyResolver {
const proto = parsedUrl.protocol.split(':', 1)[0];
if (!proto) return ''; // Don't proxy URLs without a protocol.
const hostname = parsedUrl.host;
const port = parseInt(parsedUrl.port || '') || DEFAULT_PORTS[proto] || 0;
const port = Number.parseInt(parsedUrl.port || '', 10) || DEFAULT_PORTS[proto] || 0;
if (options?.noProxy && !this.shouldProxy(hostname, port, options.noProxy)) return '';
if (proto === 'http' && options?.httpProxy) return options.httpProxy;
@@ -50,7 +50,7 @@ export class ProxyResolver {
const parsedProxy = proxy.match(/^(.+):(\d+)$/);
const parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy;
const parsedProxyPort = parsedProxy?.[2] ? parseInt(parsedProxy[2]) : 0;
const parsedProxyPort = parsedProxy?.[2] ? Number.parseInt(parsedProxy[2], 10) : 0;
if (parsedProxyPort && parsedProxyPort !== port) return true; // Skip if ports don't match.

View File

@@ -1,167 +0,0 @@
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
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, sanitizeGitHubVariables } from './helpers';
async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSClient, webIdentityToken: string) {
delete params.Tags;
core.info('Assuming role with OIDC');
try {
const creds = await client.send(
new AssumeRoleWithWebIdentityCommand({
...params,
WebIdentityToken: webIdentityToken,
}),
);
return creds;
} catch (error) {
throw new Error(`Could not assume role with OIDC: ${errorMessage(error)}`);
}
}
async function assumeRoleWithWebIdentityTokenFile(
params: AssumeRoleCommandInput,
client: STSClient,
webIdentityTokenFile: string,
workspace: string,
) {
core.debug(
'webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.',
);
const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile)
? webIdentityTokenFile
: path.join(workspace, webIdentityTokenFile);
if (!fs.existsSync(webIdentityTokenFilePath)) {
throw new Error(`Web identity token file does not exist: ${webIdentityTokenFilePath}`);
}
core.info('Assuming role with web identity token file');
try {
const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8');
delete params.Tags;
const creds = await client.send(
new AssumeRoleWithWebIdentityCommand({
...params,
WebIdentityToken: webIdentityToken,
}),
);
return creds;
} catch (error) {
throw new Error(`Could not assume role with web identity token file: ${errorMessage(error)}`);
}
}
async function assumeRoleWithCredentials(params: AssumeRoleCommandInput, client: STSClient) {
core.info('Assuming role with user credentials');
try {
const creds = await client.send(new AssumeRoleCommand({ ...params }));
return creds;
} catch (error) {
throw new Error(`Could not assume role with user credentials: ${errorMessage(error)}`);
}
}
export interface assumeRoleParams {
credentialsClient: CredentialsClient;
roleToAssume: string;
roleDuration: number;
roleSessionName: string;
roleSkipSessionTagging?: boolean;
sourceAccountId?: string;
roleExternalId?: string;
webIdentityTokenFile?: string;
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: { arn: string }[];
}
export async function assumeRole(params: assumeRoleParams) {
const {
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
} = { ...params };
// Load GitHub environment variables
const { GITHUB_REPOSITORY, GITHUB_WORKFLOW, GITHUB_ACTION, GITHUB_ACTOR, GITHUB_SHA, GITHUB_WORKSPACE } = process.env;
if (!GITHUB_REPOSITORY || !GITHUB_WORKFLOW || !GITHUB_ACTION || !GITHUB_ACTOR || !GITHUB_SHA || !GITHUB_WORKSPACE) {
throw new Error('Missing required environment variables. Are you running in GitHub Actions?');
}
// Load role session tags
const tagArray: Tag[] = [
{ Key: 'GitHub', Value: 'Actions' },
{ Key: 'Repository', Value: GITHUB_REPOSITORY },
{ Key: 'Workflow', Value: sanitizeGitHubVariables(GITHUB_WORKFLOW) },
{ Key: 'Action', Value: GITHUB_ACTION },
{ Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) },
{ Key: 'Commit', Value: GITHUB_SHA },
];
if (process.env.GITHUB_REF) {
tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
}
const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) {
core.debug('Role session tagging has been skipped.');
} else {
core.debug(`${tags.length} role session tags are being used.`);
}
// Calculate role ARN from name and account ID (currently only supports `aws` partition)
let roleArn = roleToAssume;
if (!roleArn.startsWith('arn:aws')) {
assert(
isDefined(sourceAccountId),
'Source Account ID is needed if the Role Name is provided and not the Role Arn.',
);
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
}
// Ready common parameters to assume role
const commonAssumeRoleParams: AssumeRoleCommandInput = {
RoleArn: roleArn,
RoleSessionName: roleSessionName,
DurationSeconds: roleDuration,
Tags: tags ? tags : undefined,
ExternalId: roleExternalId ? roleExternalId : undefined,
Policy: inlineSessionPolicy ? inlineSessionPolicy : undefined,
PolicyArns: managedSessionPolicies?.length ? managedSessionPolicies : undefined,
};
const keys = Object.keys(commonAssumeRoleParams) as Array<keyof typeof commonAssumeRoleParams>;
keys.forEach((k) => {
if (commonAssumeRoleParams[k] === undefined) {
delete commonAssumeRoleParams[k];
}
});
// Instantiate STS client
const stsClient = credentialsClient.stsClient;
// Assume role using one of three methods
if (!!webIdentityToken) {
return assumeRoleWithOIDC(commonAssumeRoleParams, stsClient, webIdentityToken);
}
if (!!webIdentityTokenFile) {
return assumeRoleWithWebIdentityTokenFile(
commonAssumeRoleParams,
stsClient,
webIdentityTokenFile,
GITHUB_WORKSPACE,
);
}
return assumeRoleWithCredentials(commonAssumeRoleParams, stsClient);
}

View File

@@ -1,13 +1,20 @@
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import * as core from '@actions/core';
import type { Credentials, STSClient } from '@aws-sdk/client-sts';
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import type { CredentialsClient } from './CredentialsClient';
import { type Credentials, GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { ProxyAgent } from 'proxy-agent';
import { ProxyResolver } from './ProxyResolver';
const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
export function translateEnvVariables() {
export function importEnvVariables() {
const envVars = [
'AWS_REGION',
'ROLE_TO_ASSUME',
@@ -40,56 +47,93 @@ export function translateEnvVariables() {
}
}
}
export function getActionInputs() {
const options = {
AccessKeyId: getInput('aws-access-key-id'),
SecretAccessKey: getInput('aws-secret-access-key'),
SessionToken: getInput('aws-session-token'),
region: getInput('aws-region', { required: true }) as string,
roleToAssume: getInput('role-to-assume'),
audience: getInput('audience'),
maskAccountId: getBooleanInput('mask-aws-account-id'),
roleExternalId: getInput('role-external-id'),
webIdentityTokenFile: getInput('web-identity-token-file'),
roleDuration: getNumberInput('role-duration-seconds', { default: DEFAULT_ROLE_DURATION }),
roleSessionName: getInput('role-session-name', { default: ROLE_SESSION_NAME }),
roleSkipSessionTagging: getBooleanInput('role-skip-session-tagging'),
proxyServer: getInput('http-proxy', { default: process.env.HTTP_PROXY }),
inlineSessionPolicy: getInput('inline-session-policy'),
managedSessionPolicies: getMultilineInput('managed-session-policies')?.map((p) => ({ arn: p })),
roleChaining: getBooleanInput('role-chaining'),
outputCredentials: getBooleanInput('output-credentials'),
unsetCurrentCredentials: getBooleanInput('unset-current-credentials'),
exportEnvCredentials: getBooleanInput('output-env-credentials', { default: true }),
disableRetry: getBooleanInput('disable-retry'),
maxRetries: getNumberInput('retry-max-attempts', { default: 12 }) as number,
specialCharacterWorkaround: getBooleanInput('special-characters-workaround'),
useExistingCredentials: getBooleanInput('use-existing-credentials'),
expectedAccountIds: getInput('allowed-account-ids')
?.split(',')
.map((s) => s.trim()),
forceSkipOidc: getBooleanInput('force-skip-oidc'),
noProxy: getInput('no-proxy'),
globalTimeout: getNumberInput('action-timeout-s', { default: 0 }) as number,
} as const;
// Checks
if (options.forceSkipOidc && options.roleToAssume && !options.AccessKeyId && !options.webIdentityTokenFile) {
throw new Error(
"If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set",
);
}
if (!options.roleToAssume && options.AccessKeyId && !options.SecretAccessKey) {
throw new Error(
"'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided and not assuming a role",
);
}
if (!options.region?.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${options.region}`);
}
return options;
}
// 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>,
outputCredentials?: boolean,
outputEnvCredentials?: boolean,
) {
export function exportCredentials(creds?: Partial<Credentials>) {
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', '');
}
}
if (outputEnvCredentials) {
if (creds?.AccessKeyId) {
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.exportVariable('AWS_SECRET_ACCESS_KEY', creds.SecretAccessKey);
}
if (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 outputCredentials(creds?: Partial<Credentials>) {
if (creds?.AccessKeyId) {
core.setSecret(creds.AccessKeyId);
core.setOutput('aws-access-key-id', creds.AccessKeyId);
}
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);
}
if (creds?.Expiration) {
core.setOutput('aws-expiration', creds.Expiration);
}
if (creds?.SecretAccessKey) {
core.setSecret(creds.SecretAccessKey);
core.setOutput('aws-secret-access-key', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.setSecret(creds.SessionToken);
core.setOutput('aws-session-token', creds.SessionToken);
}
if (creds?.Expiration) {
core.setOutput('aws-expiration', creds.Expiration);
}
}
@@ -103,14 +147,17 @@ export function unsetCredentials(outputEnvCredentials?: boolean) {
}
}
export function exportRegion(region: string, outputEnvCredentials?: boolean) {
if (outputEnvCredentials) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
}
export function exportRegion(region: string) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
}
export async function getCallerIdentity(client: STSClient): Promise<{ Account: string; Arn: string; UserId?: string }> {
export function outputRegion(region: string) {
core.setOutput('aws-region', region);
core.setOutput('aws-default-region', region);
}
async function getCallerIdentity(client: STSClient): Promise<{ Account: string; Arn: string; UserId?: string }> {
const identity = await client.send(new GetCallerIdentityCommand({}));
if (!identity.Account || !identity.Arn) {
throw new Error('Could not get Account ID or ARN from STS. Did you set credentials?');
@@ -125,18 +172,37 @@ export async function getCallerIdentity(client: STSClient): Promise<{ Account: s
return result;
}
// Obtains account ID from STS Client and sets it as output
export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean) {
const identity = await getCallerIdentity(credentialsClient.stsClient);
const accountId = identity.Account;
const arn = identity.Arn;
if (maskAccountId) {
export function outputAccountId(accountId: string, opts: { arn?: string | undefined; maskAccountId?: boolean }) {
if (opts.maskAccountId) {
core.setSecret(accountId);
core.setSecret(arn);
if (opts.arn) core.setSecret(opts.arn);
}
core.setOutput('aws-account-id', accountId);
core.setOutput('authenticated-arn', arn);
return accountId;
core.setOutput('authenticated-arn', opts.arn);
}
export function exportAccountId(accountId: string, opts: { maskAccountId?: boolean }) {
if (opts.maskAccountId) {
core.setSecret(accountId);
}
core.exportVariable('AWS_ACCOUNT_ID', accountId);
}
// Obtains account ID and AssumedRole ARN from STS
export async function getAccountIdFromCredentials(
credentials: AwsCredentialIdentity,
args?: { requestHandler?: NodeHttpHandler; region?: string },
): Promise<{ Account: string; Arn?: string | undefined } | undefined> {
const requestHandler = args?.requestHandler;
const region = args?.region;
const client = new STSClient({ credentials, ...(requestHandler && { requestHandler }), ...(region && { region }) });
try {
const result = await getCallerIdentity(client);
return { Account: result.Account, Arn: result.Arn };
} catch (error) {
core.debug(`getAccountIdFromCredentials: ${errorMessage(error)}`);
return undefined;
}
}
// Tags have a more restrictive set of acceptable characters than GitHub environment variables can.
@@ -148,20 +214,7 @@ export function sanitizeGitHubVariables(name: string) {
return nameTruncated;
}
export async function defaultSleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let sleep = defaultSleep;
export function withsleep(s: typeof sleep) {
sleep = s;
}
export function reset() {
sleep = defaultSleep;
}
export function verifyKeys(creds: Partial<Credentials> | undefined) {
export function verifyKeys(creds: { AccessKeyId?: string; SecretAccessKey?: string } | undefined) {
if (!creds) {
return false;
}
@@ -204,7 +257,7 @@ export async function retryAndBackoff<T>(
`Retrying after ${Math.floor(delay)}ms.`,
);
await sleep(delay);
await new Promise((r) => setTimeout(r, delay));
if (nextRetry >= maxRetries) {
core.debug('retryAndBackoff: reached max retries; giving up.');
@@ -215,20 +268,18 @@ export async function retryAndBackoff<T>(
}
}
/* 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 areCredentialsValid(credentialsClient: CredentialsClient) {
const client = credentialsClient.stsClient;
// Note: requires that credentials are already resolvable with the default chain.
export async function areCredentialsValid(args: { requestHandler?: NodeHttpHandler; region?: string }) {
// GetCallerIdentity does not require any permissions
const requestHandler = args.requestHandler;
const region = args.region;
const client = new STSClient({ ...(requestHandler && { requestHandler }), ...(region && { region }) });
try {
const identity = await client.send(new GetCallerIdentityCommand({}));
const identity = await getCallerIdentity(client);
if (identity.Account) {
return true;
}
@@ -238,6 +289,21 @@ export async function areCredentialsValid(credentialsClient: CredentialsClient)
}
}
export function configureProxy(proxyServer?: string, noProxy?: string): NodeHttpHandler {
if (proxyServer) {
core.info('Configuring proxy handler');
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
httpProxy: proxyServer,
httpsProxy: proxyServer,
...(noProxy && { noProxy }),
};
const getProxyForUrl = new ProxyResolver(proxyOptions).getProxyForUrl;
const handler = new ProxyAgent({ getProxyForUrl });
return new NodeHttpHandler({ httpsAgent: handler, httpAgent: handler });
}
return new NodeHttpHandler();
}
/**
* Like core.getBooleanInput, but respects the required option.
*
@@ -266,3 +332,87 @@ export function getBooleanInput(name: string, options?: core.InputOptions & { de
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``,
);
}
// As above, but for other input types
export function getInput(
name: string,
options?: core.InputOptions & { default?: string | undefined },
): string | undefined {
const input = core.getInput(name, options);
if (input === '') return options?.default;
return input;
}
export function getNumberInput(
name: string,
options?: core.InputOptions & { default?: number | undefined },
): number | undefined {
const input = core.getInput(name, options);
if (input === '') return options?.default;
// biome-ignore lint/correctness/useParseIntRadix: intentionally support 0x
return Number.parseInt(input);
}
export function getMultilineInput(name: string, options?: core.InputOptions): string[] | undefined {
const input = getInput(name, options)
?.split('\n')
.filter((i) => i !== '');
if (options?.trimWhitespace === false) return input;
return input?.map((i) => i.trim());
}
export function useGitHubOIDCProvider(options: ReturnType<typeof getActionInputs>): boolean {
if (options.forceSkipOidc) return false;
if (
!!options.roleToAssume &&
!options.webIdentityTokenFile &&
!options.AccessKeyId &&
!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!options.roleChaining
) {
core.info(
'It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission? ' +
'If you are not trying to authenticate with OIDC and the action is working successfully, you can ignore this message.',
);
}
return (
!!options.roleToAssume &&
!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!options.AccessKeyId &&
!options.webIdentityTokenFile &&
!options.roleChaining
);
}
export async function getGHToken(options: ReturnType<typeof getActionInputs>): Promise<string> {
let webIdentityToken: string;
try {
webIdentityToken = await retryAndBackoff(
async () => core.getIDToken(options.audience),
!options.disableRetry,
options.maxRetries,
);
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
return webIdentityToken;
}
export async function getFileToken(options: ReturnType<typeof getActionInputs>): Promise<string> {
if (!options.webIdentityTokenFile) throw new Error('webIdentityTokenFile not provided');
core.debug(
'webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.',
);
const workspace = process.env.GITHUB_WORKSPACE ?? '';
const webIdentityTokenFilePath = path.isAbsolute(options.webIdentityTokenFile)
? options.webIdentityTokenFile
: path.join(workspace, options.webIdentityTokenFile);
if (!existsSync(webIdentityTokenFilePath)) {
throw new Error(`webIdentityTokenFile does not exist: ${webIdentityTokenFilePath}`);
}
core.info('Assuming role with web identity token file');
try {
const webIdentityToken = readFileSync(webIdentityTokenFilePath, 'utf8');
return webIdentityToken;
} catch (error) {
throw new Error(`Could not read web identity token file: ${errorMessage(error)}`);
}
}

View File

@@ -1,136 +1,58 @@
import * as core from '@actions/core';
import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts';
import { assumeRole } from './assumeRole';
import { CredentialsClient } from './CredentialsClient';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { CredentialsClient, type CredentialsClientProps } from './CredentialsClient';
import {
areCredentialsValid,
configureProxy,
errorMessage,
exportAccountId,
exportCredentials,
exportRegion,
getBooleanInput,
getAccountIdFromCredentials,
getActionInputs,
getFileToken,
getGHToken,
importEnvVariables,
outputAccountId,
outputCredentials,
outputRegion,
retryAndBackoff,
translateEnvVariables,
unsetCredentials,
useGitHubOIDCProvider,
verifyKeys,
} from './helpers';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
export async function run() {
export async function run(): Promise<void> {
try {
translateEnvVariables();
// Get inputs
// Undefined inputs are empty strings ( or empty arrays)
const AccessKeyId = core.getInput('aws-access-key-id', { required: false });
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: true });
const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false });
const maskAccountId = getBooleanInput('mask-aws-account-id', { required: false });
const roleExternalId = core.getInput('role-external-id', { required: false });
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const roleDuration =
Number.parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
const roleSkipSessionTagging = getBooleanInput('role-skip-session-tagging', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false }) || process.env.HTTP_PROXY;
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false }).map((p) => {
return { arn: p };
});
const roleChaining = getBooleanInput('role-chaining', { required: false });
const outputCredentials = getBooleanInput('output-credentials', { required: false });
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: true });
const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false });
let disableRetry = getBooleanInput('disable-retry', { required: false });
const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false });
const useExistingCredentials = core.getInput('use-existing-credentials', { required: false });
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
const expectedAccountIds = core
.getInput('allowed-account-ids', { required: false })
.split(',')
.map((s) => s.trim());
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
const noProxy = core.getInput('no-proxy', { required: false });
const globalTimeout = Number.parseInt(core.getInput('action-timeout-s', { required: false })) || 0;
// Set up inputs
importEnvVariables();
const options = getActionInputs();
let maxRetries = options.maxRetries;
let disableRetry = options.disableRetry;
let timeoutId: NodeJS.Timeout | undefined;
if (globalTimeout > 0) {
core.info(`Setting a global timeout of ${globalTimeout} seconds for the action`);
timeoutId = setTimeout(() => {
core.setFailed(`Action timed out after ${globalTimeout} seconds`);
process.exit(1);
}, globalTimeout * 1000);
}
if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) {
throw new Error(
"If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set",
);
}
if (specialCharacterWorkaround) {
// 😳
// Set up retry handler
if (maxRetries < 1) maxRetries = 1;
if (options.specialCharacterWorkaround) {
disableRetry = false;
maxRetries = 12;
} else if (maxRetries < 1) {
maxRetries = 1;
if (maxRetries <= 1) maxRetries = 12;
}
// Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => {
if (forceSkipOidc) return false;
// 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
if (
!!roleToAssume &&
!webIdentityTokenFile &&
!AccessKeyId &&
!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? ' +
'If you are not trying to authenticate with OIDC and the action is working successfully, you can ignore this message.',
);
}
return (
!!roleToAssume &&
!!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!AccessKeyId &&
!webIdentityTokenFile &&
!roleChaining
);
};
if (unsetCurrentCredentials) {
unsetCredentials(outputEnvCredentials);
// Set up global timeout
let timeoutId: NodeJS.Timeout | undefined;
if (options.globalTimeout > 0) {
core.info(`Setting a global timeout of ${options.globalTimeout} seconds for the action`);
timeoutId = setTimeout(() => {
core.setFailed(`Action timed out after ${options.globalTimeout} seconds`);
process.exit(1);
}, options.globalTimeout * 1000);
}
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
exportRegion(region, outputEnvCredentials);
// Set up proxy.
const requestHandler = configureProxy(options.proxyServer, options.noProxy);
// Instantiate credentials client
const clientProps: { region: string; proxyServer?: string; noProxy?: string } = { region };
if (proxyServer) clientProps.proxyServer = proxyServer;
if (noProxy) clientProps.noProxy = noProxy;
const credentialsClient = new CredentialsClient(clientProps);
let sourceAccountId: string;
let webIdentityToken: string;
//if the user wants to attempt to use existing credentials, check if we have some already
if (useExistingCredentials) {
const validCredentials = await areCredentialsValid(credentialsClient);
if (validCredentials) {
// if the user wants to attempt to use existing credentials, check if we have some already.
// Requires STS. Uses default credential provider resolution chain.
if (options.useExistingCredentials) {
if (await areCredentialsValid({ requestHandler, region: options.region })) {
core.notice('Pre-existing credentials are valid. No need to generate new ones.');
if (timeoutId) clearTimeout(timeoutId);
return;
@@ -138,105 +60,91 @@ export async function run() {
core.notice('No valid credentials exist. Running as normal.');
}
// If OIDC is being used, generate token
// Else, export credentials provided as input
if (useGitHubOIDCProvider()) {
try {
webIdentityToken = await retryAndBackoff(
async () => {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries,
);
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
} else if (AccessKeyId) {
if (!SecretAccessKey) {
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");
}
// The STS client for calling AssumeRole pulls creds from the environment.
// Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials to already be masked as secrets
// in any error messages.
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials);
} else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
}
// Set which flow to use
// Refer to README.md for which flow to use based on set inputs
const shouldUseGHOIDC = useGitHubOIDCProvider(options); // Checks for id-token permission
const shouldUseTokenFile = !!options.webIdentityTokenFile;
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, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
}
// Get role credentials if configured to do so
if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput;
do {
roleCredentials = await retryAndBackoff(
async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
});
},
!disableRetry,
maxRetries,
);
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser?.AssumedRoleId}`);
exportCredentials(roleCredentials.Credentials, outputCredentials, outputEnvCredentials);
// 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.
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
if (!process.env.GITHUB_ACTIONS || AccessKeyId) {
await credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
);
}
if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId);
let clientProps: CredentialsClientProps;
if (options.roleToAssume) {
const assumeRoleProps = {
roleArn: options.roleToAssume,
...(options.roleSessionName && { roleSessionName: options.roleSessionName }),
...(options.roleDuration && { durationSeconds: options.roleDuration }),
region: options.region,
requestHandler,
...(options.inlineSessionPolicy && { policy: options.inlineSessionPolicy }),
...(options.managedSessionPolicies && { policyArns: options.managedSessionPolicies }),
};
if (shouldUseGHOIDC || shouldUseTokenFile) {
const token = shouldUseGHOIDC ? await getGHToken(options) : await getFileToken(options);
clientProps = { mode: 'web-identity', webIdentityToken: token, ...assumeRoleProps };
} else {
clientProps = { mode: 'sts', ...assumeRoleProps };
}
} else {
core.info('Proceeding with IAM user credentials');
// TODO: falling back to env only is a breaking change. Decide if fallback to the default
// chain is better.
clientProps = { mode: 'env' };
disableRetry = true; // Retrying the environment credentials is invalid
}
// TODO: add mode 'instance-metadata' and 'container-metadata' here
// Get the credentials
const fetchCredentials = new CredentialsClient(clientProps).getCredentials();
let credentials: AwsCredentialIdentity;
do {
credentials = await retryAndBackoff(async () => await fetchCredentials(), !disableRetry, maxRetries);
} while (
options.specialCharacterWorkaround &&
!verifyKeys({ AccessKeyId: credentials.accessKeyId, SecretAccessKey: credentials.secretAccessKey })
);
// Not all credential sources have a account ID attached, so this may require another STS call
const accountId = await getAccountIdFromCredentials(credentials, { region: options.region, requestHandler });
if (options.expectedAccountIds) {
if (!options.expectedAccountIds.includes(accountId?.Account ?? '')) {
throw new Error(`Account ID ${accountId?.Account} is not in the list of expected account IDs`);
}
}
// Clear timeout on successful completion
// Export the credentials
// Note: output refers to action outputs, export refers to process.env
if (options.exportEnvCredentials) {
exportCredentials({
AccessKeyId: credentials.accessKeyId,
SecretAccessKey: credentials.secretAccessKey,
Expiration: credentials.expiration,
SessionToken: credentials.sessionToken,
});
exportRegion(options.region);
if (accountId) exportAccountId(accountId.Account, { maskAccountId: options.maskAccountId });
}
if (options.outputCredentials) {
outputCredentials({
AccessKeyId: credentials.accessKeyId,
SecretAccessKey: credentials.secretAccessKey,
Expiration: credentials.expiration,
SessionToken: credentials.sessionToken,
});
outputRegion(options.region);
if (accountId) {
outputAccountId(accountId.Account, { arn: accountId.Arn, maskAccountId: options.maskAccountId });
}
}
// Clear global timeout
if (timeoutId) clearTimeout(timeoutId);
} catch (error) {
core.setFailed(errorMessage(error));
const showStackTrace = process.env.SHOW_STACK_TRACE;
if (showStackTrace === 'true') {
throw error;
}
if (process.env.SHOW_STACK_TRACE) throw error;
}
}
/* c8 ignore start */
/* istanbul ignore next */
if (require.main === module) {
(async () => {
await run();
})().catch((error) => {
core.setFailed(errorMessage(error));
})().catch((e) => {
core.setFailed(errorMessage(e));
});
}

243
src/index.ts.old Normal file
View File

@@ -0,0 +1,243 @@
import * as core from '@actions/core';
import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts';
import { assumeRole } from './assumeRole';
import { CredentialsClient } from './CredentialsClient';
import {
areCredentialsValid,
errorMessage,
exportAccountId,
exportCredentials,
exportRegion,
getBooleanInput,
retryAndBackoff,
importEnvVariables,
unsetCredentials,
verifyKeys,
} from './helpers';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
export async function run() {
try {
importEnvVariables();
// Get inputs
// Undefined inputs are empty strings ( or empty arrays)
const AccessKeyId = core.getInput('aws-access-key-id', { required: false });
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: true });
const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false });
const maskAccountId = getBooleanInput('mask-aws-account-id', { required: false });
const roleExternalId = core.getInput('role-external-id', { required: false });
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const roleDuration =
Number.parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
const roleSkipSessionTagging = getBooleanInput('role-skip-session-tagging', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false }) || process.env.HTTP_PROXY;
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false }).map((p) => {
return { arn: p };
});
const roleChaining = getBooleanInput('role-chaining', { required: false });
const outputCredentials = getBooleanInput('output-credentials', { required: false });
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: true });
const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false });
let disableRetry = getBooleanInput('disable-retry', { required: false });
const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false });
const useExistingCredentials = core.getInput('use-existing-credentials', { required: false });
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
const expectedAccountIds = core
.getInput('allowed-account-ids', { required: false })
.split(',')
.map((s) => s.trim());
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
const noProxy = core.getInput('no-proxy', { required: false });
const globalTimeout = Number.parseInt(core.getInput('action-timeout-s', { required: false })) || 0;
let timeoutId: NodeJS.Timeout | undefined;
if (globalTimeout > 0) {
core.info(`Setting a global timeout of ${globalTimeout} seconds for the action`);
timeoutId = setTimeout(() => {
core.setFailed(`Action timed out after ${globalTimeout} seconds`);
process.exit(1);
}, globalTimeout * 1000);
}
if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) {
throw new Error(
"If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set",
);
}
if (specialCharacterWorkaround) {
// 😳
disableRetry = false;
maxRetries = 12;
} else if (maxRetries < 1) {
maxRetries = 1;
}
// Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => {
if (forceSkipOidc) return false;
// 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
if (
!!roleToAssume &&
!webIdentityTokenFile &&
!AccessKeyId &&
!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? ' +
'If you are not trying to authenticate with OIDC and the action is working successfully, you can ignore this message.',
);
}
return (
!!roleToAssume &&
!!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!AccessKeyId &&
!webIdentityTokenFile &&
!roleChaining
);
};
if (unsetCurrentCredentials) {
unsetCredentials(outputEnvCredentials);
}
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
exportRegion(region, outputEnvCredentials);
// Instantiate credentials client
const clientProps: { region: string; proxyServer?: string; noProxy?: string } = { region };
if (proxyServer) clientProps.proxyServer = proxyServer;
if (noProxy) clientProps.noProxy = noProxy;
const credentialsClient = new CredentialsClient(clientProps);
let sourceAccountId: string;
let webIdentityToken: string;
//if the user wants to attempt to use existing credentials, check if we have some already
if (useExistingCredentials) {
//const validCredentials = await areCredentialsValid(credentialsClient);
const validCredentials = true;
if (validCredentials) {
core.notice('Pre-existing credentials are valid. No need to generate new ones.');
if (timeoutId) clearTimeout(timeoutId);
return;
}
core.notice('No valid credentials exist. Running as normal.');
}
// If OIDC is being used, generate token
// Else, export credentials provided as input
if (useGitHubOIDCProvider()) {
try {
webIdentityToken = await retryAndBackoff(
async () => {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries,
);
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
} else if (AccessKeyId) {
if (!SecretAccessKey) {
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");
}
// The STS client for calling AssumeRole pulls creds from the environment.
// Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials to already be masked as secrets
// in any error messages.
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials);
} else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
}
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, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
}
// Get role credentials if configured to do so
if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput;
do {
roleCredentials = await retryAndBackoff(
async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
});
},
!disableRetry,
maxRetries,
);
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser?.AssumedRoleId}`);
exportCredentials(roleCredentials.Credentials, outputCredentials, outputEnvCredentials);
// 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.
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
if (!process.env.GITHUB_ACTIONS || AccessKeyId) {
await credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
);
}
if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId);
}
} else {
core.info('Proceeding with IAM user credentials');
}
// Clear timeout on successful completion
if (timeoutId) clearTimeout(timeoutId);
} catch (error) {
core.setFailed(errorMessage(error));
const showStackTrace = process.env.SHOW_STACK_TRACE;
if (showStackTrace === 'true') {
throw error;
}
}
}
/* c8 ignore start */
/* istanbul ignore next */
if (require.main === module) {
(async () => {
await run();
})().catch((error) => {
core.setFailed(errorMessage(error));
});
}