diff --git a/package-lock.json b/package-lock.json index 1c6c16c..b6edce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@actions/core": "^3.0.1", "@aws-sdk/client-sts": "^3.1116.0", + "@aws-sdk/credential-provider-node": "^3.972.63", "@smithy/node-http-handler": "^4.11.3", "proxy-agent": "^8.0.2" }, diff --git a/package.json b/package.json index c18ea26..7e1a193 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dependencies": { "@actions/core": "^3.0.1", "@aws-sdk/client-sts": "^3.1116.0", + "@aws-sdk/credential-provider-node": "^3.972.63", "@smithy/node-http-handler": "^4.11.3", "proxy-agent": "^8.0.2" }, diff --git a/src/CredentialsClient.ts b/src/CredentialsClient.ts index 8347a99..ef8beed 100644 --- a/src/CredentialsClient.ts +++ b/src/CredentialsClient.ts @@ -1,5 +1,6 @@ import { info } from '@actions/core'; import { STSClient } from '@aws-sdk/client-sts'; +import { defaultProvider } from '@aws-sdk/credential-provider-node'; import type { AwsCredentialIdentity } from '@aws-sdk/types'; import { NodeHttpHandler } from '@smithy/node-http-handler'; import { ProxyAgent } from 'proxy-agent'; @@ -105,9 +106,15 @@ export class CredentialsClient { } 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(); + // Previously we constructed a new client, but that picks up the default provider chain including the endpoint. + // Explicitly calling the default provider chain allows us to pass in the endpoint and region as well as the + // proxy config. + return defaultProvider({ + clientConfig: { + ...(this.region !== undefined && { region: this.region }), + ...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }), + ...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }), + }, + })(); } } diff --git a/test/CredentialsClient.test.ts b/test/CredentialsClient.test.ts new file mode 100644 index 0000000..65288f0 --- /dev/null +++ b/test/CredentialsClient.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@aws-sdk/credential-provider-node', () => ({ + defaultProvider: vi.fn(() => async () => ({ accessKeyId: 'AKIA', secretAccessKey: 'secret' })), +})); + +import { defaultProvider } from '@aws-sdk/credential-provider-node'; +import { CredentialsClient } from '../src/CredentialsClient'; + +describe('CredentialsClient', {}, () => { + it('pins ambient credential resolution to the configured region and STS endpoint', {}, async () => { + const client = new CredentialsClient({ + region: 'eu-west-1', + stsEndpoint: 'https://sts.example.com', + roleChaining: false, + }); + // biome-ignore lint/suspicious/noExplicitAny: any required to call private method + await (client as any).loadCredentials(); + expect(defaultProvider).toHaveBeenCalledWith({ + clientConfig: expect.objectContaining({ region: 'eu-west-1', endpoint: 'https://sts.example.com' }), + }); + }); + + it('omits unset client config values from ambient credential resolution', {}, async () => { + const client = new CredentialsClient({ region: 'eu-west-1', roleChaining: false }); + // biome-ignore lint/suspicious/noExplicitAny: any required to call private method + await (client as any).loadCredentials(); + expect(defaultProvider).toHaveBeenLastCalledWith({ clientConfig: { region: 'eu-west-1' } }); + }); +});