From d26f2d03f801d2fb30ea6ec3f62371b53361303a Mon Sep 17 00:00:00 2001 From: peterwoodworth Date: Wed, 21 Jun 2023 16:45:34 -0700 Subject: [PATCH] feat: role-chaining --- action.yml | 29 ++++++++------- dist/cleanup/index.js | 2 +- dist/cleanup/src/CredentialsClient.d.ts | 6 ++-- dist/index.js | 48 ++++++++++++------------- src/CredentialsClient.ts | 33 ++++++++--------- src/assumeRole.ts | 2 +- src/helpers.ts | 2 +- src/index.ts | 15 ++++---- 8 files changed, 67 insertions(+), 70 deletions(-) diff --git a/action.yml b/action.yml index 4555942..bcd4afc 100644 --- a/action.yml +++ b/action.yml @@ -9,15 +9,14 @@ branding: color: orange icon: cloud inputs: - audience: - description: The audience to use for the OIDC provider - required: false - default: sts.amazonaws.com - aws-access-key-id: - description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance. - required: false aws-region: description: AWS Region, e.g. us-east-2 + required: true + role-to-assume: + description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials. + required: false + aws-access-key-id: + description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance. required: false aws-secret-access-key: description: AWS Access Key ID. This input is required if running in the GitHub hosted environment. It is optional if running in a self-hosted environment that already has AWS credentials, for example on an EC2 instance. @@ -25,6 +24,16 @@ inputs: aws-session-token: description: AWS Session Token required: false + web-identity-token-file: + description: Use the web identity token file from the provided file system path in order to assume an IAM role using a web identity, e.g. from within an Amazon EKS worker node. + required: false + role-chaining: + description: 'Use existing credentials from the environment to assume a new role' + required: false + audience: + 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 @@ -46,12 +55,6 @@ inputs: role-skip-session-tagging: description: Skip session tagging during role assumption required: false - role-to-assume: - description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials. - required: false - web-identity-token-file: - description: Use the web identity token file from the provided file system path in order to assume an IAM role using a web identity, e.g. from within an Amazon EKS worker node. - required: false inline-session-policy: description: 'Inline session policy' required: false diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 9071e50..7b7c64a 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -17616,7 +17616,7 @@ function exportRegion(region) { exports.exportRegion = exportRegion; // Obtains account ID from STS Client and sets it as output async function exportAccountId(credentialsClient, maskAccountId) { - const client = credentialsClient.getStsClient(); + const client = credentialsClient.stsClient; const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const accountId = identity.Account; if (!accountId) { diff --git a/dist/cleanup/src/CredentialsClient.d.ts b/dist/cleanup/src/CredentialsClient.d.ts index 4fbcf1b..a8bf0d0 100644 --- a/dist/cleanup/src/CredentialsClient.d.ts +++ b/dist/cleanup/src/CredentialsClient.d.ts @@ -5,10 +5,10 @@ export interface CredentialsClientProps { } export declare class CredentialsClient { region?: string; - private stsClient?; + private _stsClient?; private readonly requestHandler?; constructor(props: CredentialsClientProps); - getStsClient(): STSClient; - validateCredentials(expectedAccessKeyId?: string): Promise; + get stsClient(): STSClient; + validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean): Promise; private loadCredentials; } diff --git a/dist/index.js b/dist/index.js index 3c44f98..799675b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -16,14 +16,9 @@ const helpers_1 = __nccwpck_require__(9787); const USER_AGENT = 'configure-aws-credentials-for-github-actions'; class CredentialsClient { constructor(props) { - if (props.region) { - this.region = props.region; - } - else { - (0, core_1.info)('No region provided, using global STS endpoint'); - } + this.region = props.region; if (props.proxyServer) { - (0, core_1.info)('Configurint proxy handler for STS client'); + (0, core_1.info)('Configuring proxy handler for STS client'); const handler = new https_proxy_agent_1.HttpsProxyAgent(props.proxyServer); this.requestHandler = new node_http_handler_1.NodeHttpHandler({ httpAgent: handler, @@ -31,18 +26,17 @@ class CredentialsClient { }); } } - getStsClient() { - if (!this.stsClient) { - this.stsClient = new client_sts_1.STSClient({ - region: this.region ? this.region : undefined, + get stsClient() { + if (!this._stsClient) { + this._stsClient = new client_sts_1.STSClient({ + region: this.region, customUserAgent: USER_AGENT, requestHandler: this.requestHandler ? this.requestHandler : undefined, - useGlobalEndpoint: this.region ? false : true, }); } - return this.stsClient; + return this._stsClient; } - async validateCredentials(expectedAccessKeyId) { + async validateCredentials(expectedAccessKeyId, roleChaining) { let credentials; try { credentials = await this.loadCredentials(); @@ -53,9 +47,11 @@ class CredentialsClient { catch (error) { throw new Error(`Credentials could not be loaded, please check your action inputs: ${(0, helpers_1.errorMessage)(error)}`); } - const actualAccessKeyId = credentials.accessKeyId; - if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { - throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action'); + if (!roleChaining) { + const actualAccessKeyId = credentials.accessKeyId; + if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { + throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action'); + } } } async loadCredentials() { @@ -197,7 +193,7 @@ async function assumeRole(params) { const keys = Object.keys(commonAssumeRoleParams); keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); // Instantiate STS client - const stsClient = credentialsClient.getStsClient(); + const stsClient = credentialsClient.stsClient; // Assume role using one of three methods switch (true) { case !!webIdentityToken: { @@ -278,7 +274,7 @@ function exportRegion(region) { exports.exportRegion = exportRegion; // Obtains account ID from STS Client and sets it as output async function exportAccountId(credentialsClient, maskAccountId) { - const client = credentialsClient.getStsClient(); + const client = credentialsClient.stsClient; const identity = await client.send(new client_sts_1.GetCallerIdentityCommand({})); const accountId = identity.Account; if (!accountId) { @@ -390,9 +386,7 @@ async function run() { 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: false }) || - process.env['AWS_REGION'] || - process.env['AWS_DEFAULT_REGION']; + 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 = core.getInput('mask-aws-account-id', { required: false }); @@ -410,6 +404,8 @@ async function run() { for (const managedSessionPolicy of managedSessionPoliciesInput) { managedSessionPolicies.push({ arn: managedSessionPolicy }); } + const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false'; + const roleChaining = roleChainingInput.toLowerCase() === 'true'; // 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. @@ -420,14 +416,16 @@ async function run() { !webIdentityTokenFile && !AccessKeyId && !disableOIDC && - !process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']) { + !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?'); } return (!!roleToAssume && !!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !AccessKeyId && !webIdentityTokenFile && - !disableOIDC); + !disableOIDC && + !roleChaining); }; // Validate and export region if (region) { @@ -463,7 +461,7 @@ async function run() { // cases where this action is on a self-hosted runner that doesn't have credentials // configured correctly, and cases where the user intended to provide input // credentials but the secrets inputs resolved to empty strings. - await credentialsClient.validateCredentials(AccessKeyId); + await credentialsClient.validateCredentials(AccessKeyId, roleChaining); sourceAccountId = await (0, helpers_1.exportAccountId)(credentialsClient, maskAccountId); } // Get role credentials if configured to do so diff --git a/src/CredentialsClient.ts b/src/CredentialsClient.ts index 52d3dcb..8782a34 100644 --- a/src/CredentialsClient.ts +++ b/src/CredentialsClient.ts @@ -13,17 +13,13 @@ export interface CredentialsClientProps { export class CredentialsClient { public region?: string; - private stsClient?: STSClient; + private _stsClient?: STSClient; private readonly requestHandler?: NodeHttpHandler; constructor(props: CredentialsClientProps) { - if (props.region) { - this.region = props.region; - } else { - info('No region provided, using global STS endpoint'); - } + this.region = props.region; if (props.proxyServer) { - info('Configurint proxy handler for STS client'); + info('Configuring proxy handler for STS client'); const handler = new HttpsProxyAgent(props.proxyServer); this.requestHandler = new NodeHttpHandler({ httpAgent: handler, @@ -32,19 +28,18 @@ export class CredentialsClient { } } - public getStsClient(): STSClient { - if (!this.stsClient) { - this.stsClient = new STSClient({ - region: this.region ? this.region : undefined, + public get stsClient(): STSClient { + if (!this._stsClient) { + this._stsClient = new STSClient({ + region: this.region, customUserAgent: USER_AGENT, requestHandler: this.requestHandler ? this.requestHandler : undefined, - useGlobalEndpoint: this.region ? false : true, }); } - return this.stsClient; + return this._stsClient; } - public async validateCredentials(expectedAccessKeyId?: string) { + public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) { let credentials; try { credentials = await this.loadCredentials(); @@ -55,12 +50,12 @@ export class CredentialsClient { throw new Error(`Credentials could not be loaded, please check your action inputs: ${errorMessage(error)}`); } - const actualAccessKeyId = credentials.accessKeyId; + if (!roleChaining) { + const actualAccessKeyId = credentials.accessKeyId; - if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { - throw new Error( - 'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action' - ); + if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { + throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action'); + } } } diff --git a/src/assumeRole.ts b/src/assumeRole.ts index ea44764..f99b8cc 100644 --- a/src/assumeRole.ts +++ b/src/assumeRole.ts @@ -139,7 +139,7 @@ export async function assumeRole(params: assumeRoleParams) { keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); // Instantiate STS client - const stsClient = credentialsClient.getStsClient(); + const stsClient = credentialsClient.stsClient; // Assume role using one of three methods switch (true) { diff --git a/src/helpers.ts b/src/helpers.ts index 952c29a..ff61c38 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -35,7 +35,7 @@ export function exportRegion(region: string) { // Obtains account ID from STS Client and sets it as output export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: string) { - const client = credentialsClient.getStsClient(); + const client = credentialsClient.stsClient; const identity = await client.send(new GetCallerIdentityCommand({})); const accountId = identity.Account; if (!accountId) { diff --git a/src/index.ts b/src/index.ts index 4211a1b..5f34466 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,7 @@ export async function run() { 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: false }) || - process.env['AWS_REGION'] || - process.env['AWS_DEFAULT_REGION']; + 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 = core.getInput('mask-aws-account-id', { required: false }); @@ -35,6 +32,8 @@ export async function run() { for (const managedSessionPolicy of managedSessionPoliciesInput) { managedSessionPolicies.push({arn: managedSessionPolicy}); } + const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false'; + const roleChaining = roleChainingInput.toLowerCase() === 'true'; // Logic to decide whether to attempt to use OIDC or not const useGitHubOIDCProvider = () => { @@ -47,7 +46,8 @@ export async function run() { !webIdentityTokenFile && !AccessKeyId && !disableOIDC && - !process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] + !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?' @@ -58,7 +58,8 @@ export async function run() { !!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !AccessKeyId && !webIdentityTokenFile && - !disableOIDC + !disableOIDC && + !roleChaining ); }; @@ -98,7 +99,7 @@ export async function run() { // cases where this action is on a self-hosted runner that doesn't have credentials // configured correctly, and cases where the user intended to provide input // credentials but the secrets inputs resolved to empty strings. - await credentialsClient.validateCredentials(AccessKeyId); + await credentialsClient.validateCredentials(AccessKeyId, roleChaining); sourceAccountId = await exportAccountId(credentialsClient, maskAccountId); }