diff --git a/clients/client-sts/src/defaultStsRoleAssumers.ts b/clients/client-sts/src/defaultStsRoleAssumers.ts index bb7e08de7d18..84970eaf6a24 100644 --- a/clients/client-sts/src/defaultStsRoleAssumers.ts +++ b/clients/client-sts/src/defaultStsRoleAssumers.ts @@ -2,6 +2,7 @@ // Please do not touch this file. It's generated from template in: // https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -28,8 +29,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -63,19 +62,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -101,7 +102,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -164,7 +169,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/clients/client-sts/test/defaultRoleAssumers.spec.ts b/clients/client-sts/test/defaultRoleAssumers.spec.ts index 9daea2dc23fc..9a647cd57380 100644 --- a/clients/client-sts/test/defaultRoleAssumers.spec.ts +++ b/clients/client-sts/test/defaultRoleAssumers.spec.ts @@ -139,7 +139,7 @@ describe("getDefaultRoleAssumer", () => { requestHandler: handler, parentClientConfig: { region: "some-other-region", - logger: null, + logger: null as any, requestHandler: null, }, }); diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts index c3a96fec9bb9..3bebd5c381f6 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts @@ -137,7 +137,7 @@ describe("getDefaultRoleAssumer", () => { requestHandler: handler, parentClientConfig: { region: "some-other-region", - logger: null, + logger: null as any, requestHandler: null, }, }); diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts index 69a2d0019161..d8edd0789e34 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts @@ -1,4 +1,5 @@ import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -25,8 +26,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -60,19 +59,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -98,7 +99,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -161,7 +166,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/packages/credential-provider-ini/src/fromIni.integ.spec.ts b/packages/credential-provider-ini/src/fromIni.integ.spec.ts index db87a4ad5822..3609c83b91e4 100644 --- a/packages/credential-provider-ini/src/fromIni.integ.spec.ts +++ b/packages/credential-provider-ini/src/fromIni.integ.spec.ts @@ -1,43 +1,33 @@ import { STS } from "@aws-sdk/client-sts"; import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; -import { SourceProfileInit } from "@smithy/shared-ini-file-loader"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; import type { NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { PassThrough } from "node:stream"; -import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test as it } from "vitest"; import { fromIni } from "./fromIni"; let iniProfileData: ParsedIniData = null as any; -vi.mock("@smithy/shared-ini-file-loader", async () => { - const actual: any = await vi.importActual("@smithy/shared-ini-file-loader"); - const pkg = { - ...actual, - async loadSsoSessionData() { - return Object.entries(iniProfileData) - .filter(([key]) => key.startsWith("sso-session.")) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key.split("sso-session.")[1]]: value, - }), - {} - ); - }, - async parseKnownFiles(init: SourceProfileInit): Promise { - return iniProfileData; - }, - async getSSOTokenFromFile() { - return { - accessToken: "mock_sso_token", - expiresAt: "3000-01-01T00:00:00.000Z", - }; - }, - }; - return { - ...pkg, - default: pkg, - }; -}); + +function setIniProfileData(data: ParsedIniData) { + iniProfileData = data; + let buffer = ""; + for (const profile in data) { + if (profile.startsWith("sso-session.")) { + buffer += `[sso-session ${profile.split("sso-session.")[1]}]\n`; + } else { + buffer += `[profile ${profile}]\n`; + } + for (const [k, v] of Object.entries(data[profile])) { + buffer += `${k} = ${v}\n`; + } + buffer += "\n"; + } + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "config"), buffer); +} class MockNodeHttpHandler { static create(instanceOrOptions?: any) { @@ -46,6 +36,7 @@ class MockNodeHttpHandler { } return new MockNodeHttpHandler(); } + async handle(request: HttpRequest) { const body = new PassThrough({}); @@ -125,7 +116,9 @@ class MockNodeHttpHandler { }), }; } + updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} + httpHandlerConfigs(): NodeHttpHandlerOptions { return null as any; } @@ -136,22 +129,20 @@ describe("fromIni region search order", () => { process.env.AWS_PROFILE = "default"; iniProfileData = { default: { - region: "us-west-2", output: "json", + region: "us-stsar-1", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "us-stsar-1", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", }, }; - iniProfileData.assume = { - region: "us-stsar-1", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }; - Object.assign(iniProfileData.default, { - region: "us-stsar-1", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }); + setIniProfileData(iniProfileData); }); afterEach(() => { @@ -201,6 +192,7 @@ describe("fromIni region search order", () => { it("should use 3rd priority for the caller client", async () => { delete iniProfileData.default.region; + setIniProfileData(iniProfileData); const sts = new STS({ requestHandler: new MockNodeHttpHandler(), @@ -221,8 +213,78 @@ describe("fromIni region search order", () => { }); }); - it("should use 4th priority for the default partition's default region", async () => { - delete iniProfileData.default.region; + it("should use 4th priority for the config file region", async () => { + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toMatchObject({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-stsar-1", + }); + }); + + it("should use 5th priority for the AWS_REGION value", async () => { + process.env.AWS_REGION = "ap-northeast-1"; + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + setIniProfileData(iniProfileData); + + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toMatchObject({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-1", + }); + }); + + it("should use 6th priority for the default partition's default region", async () => { + delete process.env.AWS_REGION; + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + setIniProfileData(iniProfileData); const credentialsData = await fromIni({ clientConfig: { diff --git a/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts index e9fb9f38475f..feeb2a5dda92 100644 --- a/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts +++ b/packages/credential-provider-ini/src/resolveSsoCredentials.spec.ts @@ -64,7 +64,7 @@ describe(resolveSsoCredentials.name, () => { secretAccessKey: "mockSecretAccessKey", }; const requestHandler = vi.fn(); - const logger = vi.fn(); + const logger: any = vi.fn(); vi.mocked(fromSSO).mockReturnValue(() => Promise.resolve(mockCreds)); diff --git a/packages/credential-provider-node/package.json b/packages/credential-provider-node/package.json index 4ab37c76aaf8..485cfdc31602 100644 --- a/packages/credential-provider-node/package.json +++ b/packages/credential-provider-node/package.json @@ -15,7 +15,7 @@ "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo", - "test": "yarn g:vitest run", + "test": "yarn g:vitest run --reporter verbose", "test:watch": "yarn g:vitest watch", "test:integration": "yarn g:vitest run -c vitest.config.integ.mts", "test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts" diff --git a/packages/credential-provider-node/src/defaultProvider.spec.ts b/packages/credential-provider-node/src/defaultProvider.spec.ts index c078787148b1..024672d11c36 100644 --- a/packages/credential-provider-node/src/defaultProvider.spec.ts +++ b/packages/credential-provider-node/src/defaultProvider.spec.ts @@ -26,7 +26,9 @@ describe(defaultProvider.name, () => { }; const credentials = () => { - throw new CredentialsProviderError("test", true); + throw new CredentialsProviderError("test", { + tryNextLink: true, + }); }; const finalCredentials = () => { diff --git a/packages/credential-provider-node/src/defaultProvider.ts b/packages/credential-provider-node/src/defaultProvider.ts index a629d5e27f65..0e3e7ebcf03f 100644 --- a/packages/credential-provider-node/src/defaultProvider.ts +++ b/packages/credential-provider-node/src/defaultProvider.ts @@ -4,12 +4,14 @@ import type { FromIniInit } from "@aws-sdk/credential-provider-ini"; import type { FromProcessInit } from "@aws-sdk/credential-provider-process"; import type { FromSSOInit, SsoCredentialsParameters } from "@aws-sdk/credential-provider-sso"; import type { FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity"; +import type { AwsIdentityProperties } from "@aws-sdk/types"; import type { RemoteProviderInit } from "@smithy/credential-provider-imds"; -import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider"; +import { CredentialsProviderError } from "@smithy/property-provider"; import { ENV_PROFILE } from "@smithy/shared-ini-file-loader"; -import { AwsCredentialIdentity, MemoizedProvider } from "@smithy/types"; +import type { AwsCredentialIdentity } from "@smithy/types"; import { remoteProvider } from "./remoteProvider"; +import { type MemoizedRuntimeConfigAwsCredentialIdentityProvider, memoizeChain } from "./runtime/memoize-chain"; /** * @public @@ -60,9 +62,9 @@ let multipleCredentialSourceWarningEmitted = false; * @see {@link fromContainerMetadata} The function used to source credentials from the * ECS Container Metadata Service. */ -export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider => - memoize( - chain( +export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedRuntimeConfigAwsCredentialIdentityProvider => + memoizeChain( + [ async () => { const profile = init.profile ?? process.env[ENV_PROFILE]; if (profile) { @@ -95,7 +97,7 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromEnv"); return fromEnv(init)(); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromSSO"); const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoSession } = init; if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) { @@ -105,22 +107,22 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide ); } const { fromSSO } = await import("@aws-sdk/credential-provider-sso"); - return fromSSO(init)(); + return fromSSO(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni"); const { fromIni } = await import("@aws-sdk/credential-provider-ini"); - return fromIni(init)(); + return fromIni(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromProcess"); const { fromProcess } = await import("@aws-sdk/credential-provider-process"); - return fromProcess(init)(); + return fromProcess(init)(awsIdentityProperties); }, - async () => { + async (awsIdentityProperties?: AwsIdentityProperties) => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromTokenFile"); const { fromTokenFile } = await import("@aws-sdk/credential-provider-web-identity"); - return fromTokenFile(init)(); + return fromTokenFile(init)(awsIdentityProperties); }, async () => { init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::remoteProvider"); @@ -131,10 +133,9 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide tryNextLink: false, logger: init.logger, }); - } - ), - credentialsTreatedAsExpired, - credentialsWillNeedRefresh + }, + ], + credentialsTreatedAsExpired ); /** diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts new file mode 100644 index 000000000000..1b089e40b3dd --- /dev/null +++ b/packages/credential-provider-node/src/runtime/memoize-chain.spec.ts @@ -0,0 +1,159 @@ +import type { AwsIdentityProperties, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { beforeEach, describe, expect, test as it, vi } from "vitest"; + +import { credentialsWillNeedRefresh } from "../defaultProvider"; +import { memoizeChain } from "./memoize-chain"; + +describe("memoize runtime config aware AWS credential chain", () => { + let staticCredentials!: RuntimeConfigAwsCredentialIdentityProvider; + let expiringCredentials!: RuntimeConfigAwsCredentialIdentityProvider; + + const expiration = new Date(Date.now() + 5_000); + + beforeEach(() => { + vi.resetAllMocks(); + staticCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => { + await new Promise((r) => setTimeout(r, 100)); + return { + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})), + }; + }); + + let sequence = 0; + + expiringCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => { + await new Promise((r) => setTimeout(r, 100)); + return { + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: sequence++, + runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})), + }; + }); + }); + + it("should call composed provider functions", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + const credentials = await provider({ + callerClientConfig: { + region: async () => "context-region", + profile: "alt", + }, + }); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: ["callerClientConfig", "region", "profile"], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use an active lock when no credentials exist", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: [], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use a cache", async () => { + const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh); + + await Promise.all([provider(), provider(), provider(), provider(), provider()]); + const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + + expect(credentials).toEqual({ + accessKeyId: "", + secretAccessKey: "", + runtimeOptions: [], + }); + expect(staticCredentials).toHaveBeenCalledTimes(1); + }); + + it("should use a passive lock when credentials do exist", async () => { + const provider = memoizeChain([expiringCredentials], credentialsWillNeedRefresh); + + { + // initial invocation returns sequence-0 credentials. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 0, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(1); + } + + { + // second invocation returns sequence-0 credentials, but background initializes refresh. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 0, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(2); + } + + // allow new credentials to settle + await new Promise((r) => setTimeout(r, 200)); + + { + // third invocation group returns sequence-1 credentials, also with background refresh. + const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]); + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: 1, + runtimeOptions: [], + }); + } + expect(expiringCredentials).toHaveBeenCalledTimes(3); + } + }); + + it("can be force refreshed", async () => { + const provider = memoizeChain([expiringCredentials], credentialsWillNeedRefresh); + + const credentials = await Promise.all([ + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + provider({ forceRefresh: true }), + ]); + let sequence = 0; + + for (const c of credentials) { + expect(c).toEqual({ + accessKeyId: "", + secretAccessKey: "", + expiration, + sequence: sequence++, + runtimeOptions: ["forceRefresh"], + }); + } + + expect(expiringCredentials).toHaveBeenCalledTimes(5); + }); +}); diff --git a/packages/credential-provider-node/src/runtime/memoize-chain.ts b/packages/credential-provider-node/src/runtime/memoize-chain.ts new file mode 100644 index 000000000000..9f518b791470 --- /dev/null +++ b/packages/credential-provider-node/src/runtime/memoize-chain.ts @@ -0,0 +1,82 @@ +import type { + AwsCredentialIdentity, + AwsIdentityProperties, + RuntimeConfigAwsCredentialIdentityProvider, +} from "@aws-sdk/types"; + +/** + * Memoized provider chain for AWS credentials. + * The options are only reevaluated if forceRefresh=true is passed or a natural + * refresh occurs. + * + * @public + */ +export interface MemoizedRuntimeConfigAwsCredentialIdentityProvider { + (options?: AwsIdentityProperties & { forceRefresh?: boolean }): Promise; +} + +/** + * @internal + */ +export function memoizeChain( + providers: RuntimeConfigAwsCredentialIdentityProvider[], + treatAsExpired: (resolved: AwsCredentialIdentity) => boolean +): MemoizedRuntimeConfigAwsCredentialIdentityProvider { + const chain = internalCreateChain(providers) as RuntimeConfigAwsCredentialIdentityProvider; + + // exists when fetching credentials and credentials have expired or don't exist. + let activeLock: Promise | undefined; + // active when fetching credentials and valid credentials still exist. + let passiveLock: Promise | undefined; + let credentials: AwsCredentialIdentity | undefined; + + const provider = async (options?: AwsIdentityProperties & { forceRefresh?: boolean }) => { + if (options?.forceRefresh) { + return await chain(options); + } + if (credentials?.expiration) { + if (credentials?.expiration?.getTime() < Date.now()) { + credentials = undefined; + } + } + if (activeLock) { + await activeLock; + } else if (!credentials || treatAsExpired?.(credentials!)) { + if (credentials) { + if (!passiveLock) { + passiveLock = chain(options).then((c) => { + credentials = c; + passiveLock = undefined; + }); + } + } else { + activeLock = chain(options).then((c) => { + credentials = c; + activeLock = undefined; + }); + return provider(options); + } + } + return credentials!; + }; + + return provider; +} + +export const internalCreateChain = + (providers: RuntimeConfigAwsCredentialIdentityProvider[]): RuntimeConfigAwsCredentialIdentityProvider => + async (awsIdentityProperties?: AwsIdentityProperties) => { + let lastProviderError: Error | undefined; + for (const provider of providers) { + try { + return await provider(awsIdentityProperties); + } catch (err) { + lastProviderError = err; + if (err?.tryNextLink) { + continue; + } + throw err; + } + } + throw lastProviderError; + }; diff --git a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts index b9a9412097bb..02842b69c7e1 100644 --- a/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/tests/credential-provider-node.integ.spec.ts @@ -1,141 +1,24 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; -import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; -import { NodeHttpHandler } from "@smithy/node-http-handler"; import { STS, STSExtensionConfiguration } from "@aws-sdk/client-sts"; import * as credentialProviderHttp from "@aws-sdk/credential-provider-http"; import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, - fromWebToken, fromTokenFile, + fromWebToken, } from "@aws-sdk/credential-providers"; -import { HttpResponse } from "@smithy/protocol-http"; -import type { HttpRequest, MiddlewareStack, NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; +import { assumeRoleArns, MockNodeHttpHandler } from "@aws-sdk/credential-providers/tests/_test-lib"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; +import type { HttpRequest, MiddlewareStack, ParsedIniData } from "@smithy/types"; import { AdaptiveRetryStrategy, StandardRetryStrategy } from "@smithy/util-retry"; -import { PassThrough } from "node:stream"; +import child_process from "node:child_process"; +import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { join } from "node:path"; -import { createHash } from "node:crypto"; -import child_process from "node:child_process"; - -import { defaultProvider } from "../src"; - -const assumeRoleArns: string[] = []; - -class MockNodeHttpHandler { - static create(instanceOrOptions?: any) { - if (typeof instanceOrOptions?.handle === "function") { - return instanceOrOptions; - } - return new MockNodeHttpHandler(); - } - - async handle(request: HttpRequest) { - const body = new PassThrough({}); - - if (request.body?.includes("RoleArn=")) { - assumeRoleArns.push(request.body.match(/RoleArn=(.*?)&/)?.[1]); - } - - const region = (request.hostname.match(/(sts|cognito-identity|portal\.sso)\.(.*?)\./) || [, , "unknown"])[2]; - - if (request.headers.Authorization === "container-authorization") { - body.write( - JSON.stringify({ - AccessKeyId: "CONTAINER_ACCESS_KEY", - SecretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", - Token: "CONTAINER_TOKEN", - Expiration: "3000-01-01T00:00:00.000Z", - }) - ); - } else if (request.path?.includes("/federation/credentials")) { - body.write( - JSON.stringify({ - roleCredentials: { - accessKeyId: "SSO_ACCESS_KEY_ID", - secretAccessKey: "SSO_SECRET_ACCESS_KEY", - sessionToken: `SSO_SESSION_TOKEN_${region}`, - expiration: "3000-01-01T00:00:00.000Z", - }, - }) - ); - } else if (request.body?.includes("Action=AssumeRoleWithWebIdentity")) { - body.write(` - - - - STS_ARWI_ACCESS_KEY_ID - STS_ARWI_SECRET_ACCESS_KEY - STS_ARWI_SESSION_TOKEN_${region} - 3000-01-01T00:00:00.000Z - - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.body?.includes("Action=AssumeRole")) { - body.write(` - - - - STS_AR_ACCESS_KEY_ID - STS_AR_SECRET_ACCESS_KEY - STS_AR_SESSION_TOKEN_${region} - 3000-01-01T00:00:00.000Z - - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.body.includes("Action=GetCallerIdentity")) { - body.write(` - - - arn:aws:iam::123456789012:user/Alice - AIDACKCEVSQ6C2EXAMPLE - 123456789012 - - - 01234567-89ab-cdef-0123-456789abcdef - -`); - } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetCredentialsForIdentity") { - body.write(`{ - "Credentials":{ - "SecretKey":"COGNITO_SECRET_KEY", - "SessionToken":"COGNITO_SESSION_TOKEN_${region}", - "Expiration":${new Date("3000-01-01T00:00:00.000Z").getTime() / 1000}, - "AccessKeyId":"COGNITO_ACCESS_KEY_ID" - }, - "IdentityId":"${region}:COGNITO_IDENTITY_ID" - }`); - } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetId") { - body.write(`{ - "IdentityId":"${region}:COGNITO_IDENTITY_ID" - }`); - } else { - console.log(request); - throw new Error("request not supported."); - } - body.end(); - return { - response: new HttpResponse({ - statusCode: 200, - body, - headers: {}, - }), - }; - } - - updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test as it, vi } from "vitest"; - httpHandlerConfigs(): NodeHttpHandlerOptions { - return null as any; - } -} +import { defaultProvider } from "../src/defaultProvider"; describe("credential-provider-node integration test", () => { let sts: STS = null as any; @@ -805,6 +688,24 @@ describe("credential-provider-node integration test", () => { }); }); + it("prefers AWS_REGION over profile region when profile is not the credential source", async () => { + process.env.AWS_REGION = "eu-north-1"; + const provider = fromTokenFile({ + roleArn: "ROLE_ARN", + webIdentityTokenFile: "token-filepath", + }); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_eu-north-1", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + }); + }); + it("should use the caller client region if derived from AWS_REGION", async () => { process.env.AWS_REGION = "eu-west-2"; const provider = fromTokenFile({ @@ -1424,7 +1325,7 @@ describe("credential-provider-node integration test", () => { }); describe("nested STS client", () => { - it("the clientConfig is propagated to the inner STS client used for AssumeRole ", async () => { + it("the clientConfig is propagated to the inner STS client used for AssumeRole", async () => { setIniProfileData({ assume: { region: "us-stsar-1", @@ -1492,5 +1393,120 @@ describe("credential-provider-node integration test", () => { expect(logger.debug).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalled(); }); + + describe("uses a variant of the default region resolution where us-east-1 is the last resort", async () => { + it("no env or profile region", async () => { + delete process.env.AWS_REGION; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-east-1", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("env region", async () => { + process.env.AWS_REGION = "eu-north-1"; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_eu-north-1", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("profile region", async () => { + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + region: "us-west-2", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-west-2", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("profile and env region conflict, it uses config region because credentials are fromIni", async () => { + process.env.AWS_REGION = "eu-north-1"; + setIniProfileData({ + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + default: { + region: "us-west-2", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + source_profile: "assume", + }, + }); + + const provider = defaultProvider({}); + const credentials = await provider(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + expiration: new Date(`3000-01-01T00:00:00.000Z`), + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-west-2", + $source: { + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + }); }); }); diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index 4b04e81be3c5..1d2fadfb3154 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -7,26 +7,29 @@ A collection of all credential providers. # Table of Contents -1. [Terminology](#terminology) -1. [From Cognito Identity](#fromcognitoidentity) -1. [From Cognito Identity Pool](#fromcognitoidentitypool) -1. [From Temporary Credentials](#fromtemporarycredentials) -1. [From Web Token](#fromwebtoken) - 1. [Examples](#examples) -1. [From Token File](#fromtokenfile) -1. [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata) -1. [From HTTP(S)](#fromhttp) -1. [From Shared INI files](#fromini) - 1. [Sample Files](#sample-files) -1. [From Environmental Variables](#fromenv) -1. [From Credential Process](#fromprocess) - 1. [Sample files](#sample-files-1) -1. [From Single Sign-On Service](#fromsso) - 1. [Supported Configuration](#supported-configuration) - 1. [SSO login with AWS CLI](#sso-login-with-the-aws-cli) - 1. [Sample Files](#sample-files-2) -1. [From Node.js default credentials provider chain](#fromnodeproviderchain) -1. [Creating a custom credentials chain](#createcredentialchain) +- [Terminology](#terminology) + - [Credentials Provider](#credentials-provider) + - [Outer and inner clients](#outer-and-inner-clients) +- [Resolving AWS Region in credential providers (e.g. STS region)](#resolving-aws-region-in-credential-providers-eg-sts-region) +- [From Cognito Identity](#fromcognitoidentity) +- [From Cognito Identity Pool](#fromcognitoidentitypool) +- [From Temporary Credentials](#fromtemporarycredentials) +- [From Web Token](#fromwebtoken) + - [Examples](#examples) +- [From Token File](#fromtokenfile) +- [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata) +- [From HTTP(S)](#fromhttp) +- [From Shared INI files](#fromini) + - [Sample Files](#sample-files) +- [From Environmental Variables](#fromenv) +- [From Credential Process](#fromprocess) + - [Sample files](#sample-files-1) +- [From Single Sign-On Service](#fromsso) + - [Supported Configuration](#supported-configuration) + - [SSO login with AWS CLI](#sso-login-with-the-aws-cli) + - [Sample Files](#sample-files-2) +- [From Node.js default credentials provider chain](#fromnodeproviderchain) +- [Creating a custom credentials chain](#createcredentialchain) ## Terminology @@ -80,6 +83,77 @@ In the above example, `S3Client` is the outer client, and if the `fromIni` credentials provider uses STS::AssumeRole, the `STSClient` initialized by the SDK is the inner client. +## Resolving AWS Region in credential providers (e.g. STS region) + +When a credential provider uses an SDK client to retrieve credentials, commonly STS, the +control of the STS region follows this logic: + +```js +import { fromIni } from "@aws-sdk/credential-providers"; + +/* +# AWS config file contents +[profile default] +region = profile-region +role_arn = ROLE_ARN +source_profile = assume + +[profile assume] +... + */ + +process.env.AWS_REGION = "env-region"; + +const provider = fromIni({ + clientConfig: { + region: "credential-provider-config-region", + }, +}); + +const client = new SDKClient({ + region: "context-client-region", + credentials: provider, +}); + +const fallbackRegion = "us-east-1"; +``` + +As shown above, there are many sources of region information. The priority order is: + +1. `credential-provider-config-region` - given in code to the credential provider itself. +2. `profile-region` **\*** - if credential provider is resolving credentials from the config file, the config file's + region takes precedence in this case over AWS_REGION env. +3. `context-client-region` - the region resolved by an SDK client using the credential provider. +4. `env-region` - AWS_REGION environment variable. +5. `profile-region` **\*** - if credential provider is not resolving credentials from the config file, the config file's + region is lower priority than AWS_REGION env. +6. `us-east-1` (fallback) - this is a legacy fallback value. It's more likely that the client will fail to execute any + operation if none of the other region sources were set. + +This differs from _direct_ instantiation of the STSClient, which follows this order, which is the same for all clients: + +```js +import { STSClient } from "@aws-sdk/client-sts"; +/* +# AWS config file contents +[profile default] +region = profile-region + */ + +process.env.AWS_REGION = "env-region"; + +const client = new STSClient({ + region: "client-region", +}); +``` + +1. `client-region` +2. `env-region` +3. `profile-region` (config file) +4. thrown error (no us-east-1 fallback) + +# Credential providers + ## `fromCognitoIdentity()` - Uses `@aws-sdk/client-cognito-identity` @@ -752,7 +826,7 @@ import { fromSSO } from "@aws-sdk/credential-providers"; // ES6 import const client = new FooClient({ // Optional, available on clients as of v3.714.0. profile: "my-sso-profile", - credentials: fromProcess({ + credentials: fromSSO({ // Optional. Defaults to the client's profile if that is set. // You can specify a profile here as well, but this applies // only to the credential resolution and not to the upper client. @@ -898,20 +972,26 @@ const credentialProvider = fromNodeProviderChain({ You can use this helper to create a credential chain of your own. -A credential chain is created from a list of functions of the signature () => Promise<[AwsCredentialIdentity](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/AwsCredentialIdentity/)>, +A credential chain is created from a list of functions of the signature () => +Promise<[AwsCredentialIdentity](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-smithy-types/Interface/AwsCredentialIdentity/)>, composed together such that the overall chain has the **same** signature. -That is why you can provide the chained credential provider to the same field (`credentials`) as any single provider function. +That is why you can provide the chained credential provider to the same field (`credentials`) as any single provider +function. All the providers from this package are compatible, and can be used to create such a chain. -As with _any_ function provided to the `credentials` SDK client constructor configuration field, if the credential object returned does not contain -an `expiration` (type `Date`), the client will only ever call the provider function once. You do not need to memoize this function. +As with _any_ function provided to the `credentials` SDK client constructor configuration field, if the credential +object returned does not contain +an `expiration` (type `Date`), the client will only ever call the provider function once. You do not need to memoize +this function. -To enable automatic refresh, the credential provider function should set an `expiration` (`Date`) field. When this expiration approaches within 5 minutes, the +To enable automatic refresh, the credential provider function should set an `expiration` (`Date`) field. When this +expiration approaches within 5 minutes, the provider function will be called again by the client in the course of making SDK requests. -To assist with this, the `createCredentialChain` has a chainable helper `.expireAfter(milliseconds: number)`. An example is included below. +To assist with this, the `createCredentialChain` has a chainable helper `.expireAfter(milliseconds: number)`. An example +is included below. ```ts import { fromEnv, fromIni, createCredentialChain } from "@aws-sdk/credential-providers"; diff --git a/packages/credential-providers/src/createCredentialChain.ts b/packages/credential-providers/src/createCredentialChain.ts index 795d5f6115b3..47e941cb4eff 100644 --- a/packages/credential-providers/src/createCredentialChain.ts +++ b/packages/credential-providers/src/createCredentialChain.ts @@ -90,14 +90,13 @@ export const propertyProviderChain = (...providers: Array>): RuntimeConfigIdentityProvider => async (awsIdentityProperties?: AwsIdentityProperties) => { if (providers.length === 0) { - throw new ProviderError("No providers in chain"); + throw new ProviderError("No providers in chain", { tryNextLink: false }); } let lastProviderError: Error | undefined; for (const provider of providers) { try { - const credentials = await provider(awsIdentityProperties); - return credentials; + return await provider(awsIdentityProperties); } catch (err) { lastProviderError = err; if (err?.tryNextLink) { diff --git a/packages/credential-providers/src/fromSSO.ts b/packages/credential-providers/src/fromSSO.ts index af04b52cc65c..563de164873a 100644 --- a/packages/credential-providers/src/fromSSO.ts +++ b/packages/credential-providers/src/fromSSO.ts @@ -44,6 +44,6 @@ import { AwsCredentialIdentityProvider } from "@smithy/types"; * * @public */ -export const fromSSO = (init: FromSSOInit = {}): AwsCredentialIdentityProvider => { +export const fromSSO = (init: Parameters[0] = {}): AwsCredentialIdentityProvider => { return _fromSSO({ ...init }); }; diff --git a/packages/credential-providers/tests/_test-lib.ts b/packages/credential-providers/tests/_test-lib.ts new file mode 100644 index 000000000000..a3d8351a5a9b --- /dev/null +++ b/packages/credential-providers/tests/_test-lib.ts @@ -0,0 +1,495 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromSSO } from "@aws-sdk/credential-providers"; +import { warning } from "@aws-sdk/region-config-resolver"; +import { ParsedIniData, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import { HttpResponse } from "@smithy/protocol-http"; +import { externalDataInterceptor } from "@smithy/shared-ini-file-loader"; +import type { HttpRequest, NodeHttpHandlerOptions } from "@smithy/types"; +import child_process from "node:child_process"; +import { createHash } from "node:crypto"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { PassThrough } from "node:stream"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, test as it } from "vitest"; + +export const assumeRoleArns: string[] = []; +warning.silence = true; +let iniProfileData: ParsedIniData = null as any; + +export type CredentialTestParameters = { + // has caller context client + withCaller: boolean; + // has region specified on the caller client + callerClientRegion: boolean; + // AWS_REGION is set + envRegion: boolean; + // profile regions are set + profileRegion: boolean; + // provider itself has a clientConfig.region + providerRegion: boolean; + // profile name + profile: string | undefined; +}; + +/** + * Credential provider tester. + */ +export class CTest

RuntimeConfigAwsCredentialIdentityProvider> { + private readonly credentialProvider: P; + private readonly providerParams: (testParams: CredentialTestParameters) => Parameters

[0]; + private readonly profileCredentials: boolean; + private readonly filter: (testParams: CredentialTestParameters) => boolean; + private readonly fallbackRegion: string; + + public constructor({ + credentialProvider, + providerParams, + profileCredentials, + filter, + fallbackRegion, + }: { + credentialProvider: P; + providerParams?: (testParams: CredentialTestParameters) => Parameters

[0]; + profileCredentials?: boolean; + filter?: (testParams: CredentialTestParameters) => boolean; + fallbackRegion?: string; + }) { + this.credentialProvider = credentialProvider; + this.providerParams = providerParams ?? CTest.defaultRegionConfigProvider; + this.profileCredentials = !!profileCredentials; + this.filter = filter ?? (() => true); + this.fallbackRegion = fallbackRegion ?? "unresolved"; + this.init(); + } + + public static defaultRegionConfigProvider({ profile, providerRegion, withCaller }: CredentialTestParameters) { + if (withCaller) { + return { + clientConfig: { + region: providerRegion ? "provider-region" : undefined, + }, + }; + } + return { + // used by fromIni + profile, + clientConfig: { + // used by e.g. fromTemporaryCredentials that don't have top level profile selection + profile, + region: providerRegion ? "provider-region" : undefined, + }, + }; + } + + public init() { + let processSnapshot: typeof process.env = null as any; + const nodeHttpHandlerCreate = NodeHttpHandler.create; + + const RESERVED_ENVIRONMENT_VARIABLES = { + AWS_DEFAULT_REGION: 1, + AWS_REGION: 1, + AWS_PROFILE: 1, + AWS_ACCESS_KEY_ID: 1, + AWS_SECRET_ACCESS_KEY: 1, + AWS_SESSION_TOKEN: 1, + AWS_CREDENTIAL_EXPIRATION: 1, + AWS_EC2_METADATA_DISABLED: 1, + AWS_WEB_IDENTITY_TOKEN_FILE: 1, + AWS_ROLE_ARN: 1, + AWS_CONTAINER_CREDENTIALS_FULL_URI: 1, + AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: 1, + AWS_CONTAINER_AUTHORIZATION_TOKEN: 1, + AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: 1, + }; + + function copy(data: T): T { + return JSON.parse(JSON.stringify(data)); + } + + beforeAll(async () => { + processSnapshot = copy(process.env); + NodeHttpHandler.create = MockNodeHttpHandler.create; + const mockExec = ((bin: string, ...args: any[]) => { + const callback = args.find((arg) => typeof arg === "function"); + if (bin === "credential-process") { + return callback(null, { + stdout: JSON.stringify({ + Version: 1, + AccessKeyId: "PROCESS_ACCESS_KEY_ID", + SecretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + SessionToken: "PROCESS_SESSION_TOKEN", + }), + }); + } + return child_process.exec(bin, ...args); + }) as any; + + externalDataInterceptor.interceptToken("exec", mockExec); + }); + + beforeEach(async () => { + for (const variable in RESERVED_ENVIRONMENT_VARIABLES) { + delete process.env[variable]; + } + setIniProfileData({ + default: { + region: "us-west-2", + output: "json", + }, + }); + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "credentials"), ""); + externalDataInterceptor.interceptFile("token-filepath", "token-contents"); + const ssoToken = { + accessToken: "mock_sso_token", + expiresAt: "3000-01-01T00:00:00.000Z", + }; + const hasher = createHash("sha1"); + const cacheName = hasher.update("SSO_START_URL").digest("hex"); + const tokenPath = join(homedir(), ".aws", "sso", "cache", `${cacheName}.json`); + externalDataInterceptor.interceptFile(tokenPath, JSON.stringify(ssoToken)); + externalDataInterceptor.interceptToken("SSO_START_URL", ssoToken); + externalDataInterceptor.interceptToken("ssoNew", ssoToken); + externalDataInterceptor.interceptToken("token-filepath", "token-contents"); + }); + + afterEach(async () => { + Object.assign(process.env, processSnapshot); + setIniProfileData({ + default: {}, + }); + assumeRoleArns.length = 0; + }); + + afterAll(() => { + NodeHttpHandler.create = nodeHttpHandlerCreate; + delete externalDataInterceptor.getTokenRecord().exec; + }); + } + + public setIni(data: Parameters[0]) { + setIniProfileData(data); + } + + public testRegion() { + for (const withCaller of [true, false]) { + for (const callerClientRegion of [true, false]) { + if (callerClientRegion && !withCaller) { + continue; + } + for (const envRegion of [true, false]) { + for (const profileRegion of [true, false]) { + for (const providerRegion of [true, false]) { + for (const profile of ["default", "alt", undefined]) { + if (!callerClientRegion && !profileRegion && !envRegion) { + continue; + } + + const params = { + withCaller, + callerClientRegion, + envRegion, + profileRegion, + providerRegion, + profile, + }; + + if (!this.filter(params)) { + continue; + } + + it(`${serializeParams(params)}`, async () => { + const region = await this.findCredentialSourceRegion(params).catch((e) => { + return "failed"; + }); + const regionRequired = this.fallbackRegion === "unresolved" || withCaller; + const providerParams = this.providerParams(params); + const isSso = this.credentialProvider === fromSSO || providerParams.ssoStartUrl; + const hasRegion = providerRegion || profileRegion || callerClientRegion || envRegion; + + if (regionRequired && !hasRegion) { + expect(region).toBe("failed"); + } + + if (providerRegion) { + expect(region).toBe("provider-region"); + return; + } + + if (isSso) { + expect(region).toBe(providerParams.ssoRegion); + return; + } + + const usesProfileCredentials = this.profileCredentials; + + if (usesProfileCredentials && profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + if (callerClientRegion && withCaller) { + expect(region).toBe("code-region"); + return; + } + + if (envRegion) { + expect(region).toBe("env-region"); + return; + } + + if (!usesProfileCredentials && profileRegion) { + expect(region).toBe(`${profile ?? "default"}-profile-region`); + return; + } + + expect(region).toBe(this.fallbackRegion); + }); + } + } + } + } + } + } + } + + private async findCredentialSourceRegion(testParams: CredentialTestParameters) { + const { withCaller, envRegion, profile, profileRegion, callerClientRegion, providerRegion } = testParams; + + if (envRegion) { + process.env.AWS_REGION = "env-region"; + } else { + delete process.env.AWS_REGION; + } + + if (profileRegion) { + iniProfileData = { + default: { + region: "default-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + region: "assume-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + region: "alt-profile-region", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + region: "assume2-profile-region", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } else { + iniProfileData = { + default: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }, + assume: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + alt: { + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume2", + }, + assume2: { + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }, + }; + } + setIniProfileData(iniProfileData); + + if (withCaller) { + const s3 = new S3({ + profile, + region: callerClientRegion ? "code-region" : undefined, + credentials: this.credentialProvider(this.providerParams(testParams)), + }); + + await s3.listBuckets({}); + const credentials = await s3.config.credentials(); + return credentials.sessionToken!.replace(/(.*?)SESSION_TOKEN_/, ""); + } + + const provider = this.credentialProvider(this.providerParams(testParams)); + + const credentials = await provider(); + return credentials.sessionToken!.replace(/(.*?)SESSION_TOKEN_/, ""); + } +} + +export class MockNodeHttpHandler { + static create(instanceOrOptions?: any) { + if (typeof instanceOrOptions?.handle === "function") { + return instanceOrOptions; + } + return new MockNodeHttpHandler(); + } + + async handle(request: HttpRequest) { + const body = new PassThrough({}); + + if (request.body?.includes("RoleArn=")) { + assumeRoleArns.push(request.body.match(/RoleArn=(.*?)&/)?.[1]); + } + + const region = (request.hostname.match(/(sts|cognito-identity|portal\.sso)\.(.*?)\./) || [, , "unknown"])[2]; + + if (request.headers.Authorization === "container-authorization" || request.hostname === "169.254.170.23") { + body.write( + JSON.stringify({ + AccessKeyId: "CONTAINER_ACCESS_KEY", + SecretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + Token: "CONTAINER_TOKEN", + Expiration: "3000-01-01T00:00:00.000Z", + }) + ); + } else if (request.path?.includes("/federation/credentials")) { + body.write( + JSON.stringify({ + roleCredentials: { + accessKeyId: "SSO_ACCESS_KEY_ID", + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: `SSO_SESSION_TOKEN_${region}`, + expiration: "3000-01-01T00:00:00.000Z", + }, + }) + ); + } else if (request.body?.includes("Action=AssumeRoleWithWebIdentity")) { + body.write(` + + + + STS_ARWI_ACCESS_KEY_ID + STS_ARWI_SECRET_ACCESS_KEY + STS_ARWI_SESSION_TOKEN_${region} + 3000-01-01T00:00:00.000Z + + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.body?.includes("Action=AssumeRole")) { + body.write(` + + + + STS_AR_ACCESS_KEY_ID + STS_AR_SECRET_ACCESS_KEY + STS_AR_SESSION_TOKEN_${region} + 3000-01-01T00:00:00.000Z + + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.body?.includes("Action=GetCallerIdentity")) { + body.write(` + + + arn:aws:iam::123456789012:user/Alice + AIDACKCEVSQ6C2EXAMPLE + 123456789012 + + + 01234567-89ab-cdef-0123-456789abcdef + +`); + } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetCredentialsForIdentity") { + body.write(`{ + "Credentials":{ + "SecretKey":"COGNITO_SECRET_KEY", + "SessionToken":"COGNITO_SESSION_TOKEN_${region}", + "Expiration":${new Date("3000-01-01T00:00:00.000Z").getTime() / 1000}, + "AccessKeyId":"COGNITO_ACCESS_KEY_ID" + }, + "IdentityId":"${region}:COGNITO_IDENTITY_ID" + }`); + } else if (request.headers["x-amz-target"] === "AWSCognitoIdentityService.GetId") { + body.write(`{ + "IdentityId":"${region}:COGNITO_IDENTITY_ID" + }`); + } else if (request.hostname.startsWith("s3.")) { + body.write(` + + + + + xx + xx + + xx + xx +`); + } else { + console.log(request); + throw new Error("request not supported."); + } + + body.end(); + return { + response: new HttpResponse({ + statusCode: 200, + body, + headers: {}, + }), + }; + } + + updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {} + + httpHandlerConfigs(): NodeHttpHandlerOptions { + return null as any; + } +} + +function setIniProfileData(data: ParsedIniData) { + iniProfileData = data; + let buffer = "[profile memfs-test-mock]\n\n"; + for (const profile in data) { + if (profile.startsWith("sso-session.")) { + buffer += `[sso-session ${profile.split("sso-session.")[1]}]\n`; + } else { + buffer += `[profile ${profile}]\n`; + } + for (const [k, v] of Object.entries(data[profile])) { + buffer += `${k} = ${v}\n`; + } + buffer += "\n"; + } + const dir = join(homedir(), ".aws"); + externalDataInterceptor.interceptFile(join(dir, "config"), buffer); +} + +function serializeParams(params: CredentialTestParameters) { + let buffer = ""; + for (const [key, value] of Object.entries(params)) { + if (typeof value === "boolean") { + if (value) { + buffer += ` ${key},`; + } + } else { + buffer += ` ${key} = ${value},`; + } + } + return buffer; +} diff --git a/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts new file mode 100644 index 000000000000..e84e0a6594ec --- /dev/null +++ b/packages/credential-providers/tests/fromCognitoIdentity.integ.spec.ts @@ -0,0 +1,55 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromCognitoIdentity } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromCognitoIdentity.name, () => { + const ctest = new CTest({ + credentialProvider: fromCognitoIdentity, + providerParams: (testParams) => { + return { + identityId: "us-east-1:128d0a74-c82f-4553-916d-90053example", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-east-2", + credentials: fromCognitoIdentity({ + identityId: "us-east-2:128d0a74-c82f-4553-916d-90053example", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + }, + accessKeyId: "COGNITO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + identityId: "us-east-2:128d0a74-c82f-4553-916d-90053example", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_us-east-2", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts new file mode 100644 index 000000000000..4b726d2e071d --- /dev/null +++ b/packages/credential-providers/tests/fromCognitoIdentityPool.integ.spec.ts @@ -0,0 +1,55 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromCognitoIdentityPool.name, () => { + const ctest = new CTest({ + credentialProvider: fromCognitoIdentityPool, + providerParams: (testParams) => { + return { + identityPoolId: "us-east-1:1699ebc0-7900-4099-b910-2df94f52a030", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-east-2", + credentials: fromCognitoIdentityPool({ + identityPoolId: "us-east-2:COGNITO_IDENTITY_ID", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + }, + accessKeyId: "COGNITO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + identityId: "us-east-2:COGNITO_IDENTITY_ID", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_us-east-2", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts new file mode 100644 index 000000000000..4c79981f35b8 --- /dev/null +++ b/packages/credential-providers/tests/fromContainerMetadata.integ.spec.ts @@ -0,0 +1,41 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromContainerMetadata } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromContainerMetadata.name, () => { + const ctest = new CTest({ + credentialProvider: fromContainerMetadata, + providerParams: CTest.defaultRegionConfigProvider, + }); + + void S3; + void ctest; + + describe("configure from env", () => { + it.skip("should be configurable from env", async () => { + // todo: no integration hooks in this provider. + // process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://127.0.0.1"; + // process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + // + // const s3 = new S3({ + // credentials: fromContainerMetadata({}), + // }); + // await s3.listBuckets(); + // expect(await s3.config.credentials()).toEqual({}); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable form code", async () => { + expect("ok").toBeTruthy(); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromEnv.integ.spec.ts b/packages/credential-providers/tests/fromEnv.integ.spec.ts new file mode 100644 index 000000000000..ea930ea4fdd5 --- /dev/null +++ b/packages/credential-providers/tests/fromEnv.integ.spec.ts @@ -0,0 +1,41 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromEnv } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromEnv.name, () => { + const ctest = new CTest({ + credentialProvider: fromEnv, + }); + + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_ACCESS_KEY_ID = "AK"; + process.env.AWS_SECRET_ACCESS_KEY = "SK"; + process.env.AWS_SESSION_TOKEN = "session-token-env"; + const s3 = new S3({}); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_ENV_VARS: "g", + }, + accessKeyId: "AK", + secretAccessKey: "SK", + sessionToken: "session-token-env", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable from code", async () => { + expect("ok").toBeTruthy(); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromHttp.integ.spec.ts b/packages/credential-providers/tests/fromHttp.integ.spec.ts new file mode 100644 index 000000000000..4af5de2221dd --- /dev/null +++ b/packages/credential-providers/tests/fromHttp.integ.spec.ts @@ -0,0 +1,60 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromHttp } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromHttp.name, () => { + const ctest = new CTest({ + credentialProvider: fromHttp, + }); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://169.254.170.23"; + process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + const s3 = new S3({ + credentials: fromHttp(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_HTTP: "z", + CREDENTIALS_CODE: "e", + }, + accessKeyId: "CONTAINER_ACCESS_KEY", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + sessionToken: "CONTAINER_TOKEN", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + credentials: fromHttp({ + awsContainerCredentialsFullUri: "http://169.254.170.23", + authorizationToken: "container-authorization", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_HTTP: "z", + CREDENTIALS_CODE: "e", + }, + accessKeyId: "CONTAINER_ACCESS_KEY", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "CONTAINER_SECRET_ACCESS_KEY", + sessionToken: "CONTAINER_TOKEN", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromIni.integ.spec.ts b/packages/credential-providers/tests/fromIni.integ.spec.ts new file mode 100644 index 000000000000..ee8c500c6c78 --- /dev/null +++ b/packages/credential-providers/tests/fromIni.integ.spec.ts @@ -0,0 +1,96 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromIni } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromIni.name, () => { + const ctest = new CTest({ + credentialProvider: fromIni, + providerParams: CTest.defaultRegionConfigProvider, + profileCredentials: true, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_PROFILE = "alt"; + ctest.setIni({ + alt: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + ctest.setIni({ + default: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + ctest.setIni({ + alt: { + region: "us-west-2", + aws_access_key_id: "A", + aws_secret_access_key: "S", + aws_session_token: "T", + }, + }); + const s3 = new S3({ + profile: "alt", + credentials: fromIni(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE: "n", + }, + accessKeyId: "A", + secretAccessKey: "S", + sessionToken: "T", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts new file mode 100644 index 000000000000..3514a4963c74 --- /dev/null +++ b/packages/credential-providers/tests/fromInstanceMetadata.integ.spec.ts @@ -0,0 +1,39 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromInstanceMetadata } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromInstanceMetadata.name, () => { + const ctest = new CTest({ + credentialProvider: fromInstanceMetadata, + }); + + void ctest; + + describe("configure from env", () => { + it.skip("is configurable from env", async () => { + void S3; + // todo: there are no integration hooks in this provider. + // process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = "http://169.254.170.23"; + // process.env.AWS_CONTAINER_AUTHORIZATION_TOKEN = "container-authorization"; + // const s3 = new S3({ + // credentials: fromInstanceMetadata(), + // }); + // await s3.listBuckets(); + // expect(await s3.config.credentials()).toEqual({}); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("is not configurable from code", async () => { + expect("ok").toBeTruthy(); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts new file mode 100644 index 000000000000..0453ea427a54 --- /dev/null +++ b/packages/credential-providers/tests/fromNodeProviderChain.integ.spec.ts @@ -0,0 +1,22 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromNodeProviderChain.name, () => { + const ctest = new CTest({ + credentialProvider: fromNodeProviderChain, + providerParams: CTest.defaultRegionConfigProvider, + profileCredentials: true, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); + + void S3; + + it("is tested in the credential-provider-node.integ.spec.ts file", async () => { + expect("ok").toBeTruthy(); + }); +}); diff --git a/packages/credential-providers/tests/fromProcess.integ.spec.ts b/packages/credential-providers/tests/fromProcess.integ.spec.ts new file mode 100644 index 000000000000..eca7a513f362 --- /dev/null +++ b/packages/credential-providers/tests/fromProcess.integ.spec.ts @@ -0,0 +1,85 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromProcess } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromProcess.name, () => { + const ctest = new CTest({ + credentialProvider: fromProcess, + }); + + describe("configure from env", () => { + it("is partially configurable from env - but only in selecting a profile", async () => { + process.env.AWS_PROFILE = "alt"; + ctest.setIni({ + alt: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + credentials: fromProcess({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); + + describe("configure from profile", () => { + it("is configurable from profile", async () => { + ctest.setIni({ + default: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + credentials: fromProcess(), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); + + describe("configure from code", () => { + it("is partially configurable from code - but only in selecting a profile", async () => { + ctest.setIni({ + alt: { + credential_process: "credential-process", + }, + }); + const s3 = new S3({ + region: "us-east-2", + profile: "alt", + credentials: fromProcess({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROCESS: "w", + }, + accessKeyId: "PROCESS_ACCESS_KEY_ID", + secretAccessKey: "PROCESS_SECRET_ACCESS_KEY", + sessionToken: "PROCESS_SESSION_TOKEN", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromSSO.integ.spec.ts b/packages/credential-providers/tests/fromSSO.integ.spec.ts new file mode 100644 index 000000000000..5144a13341d0 --- /dev/null +++ b/packages/credential-providers/tests/fromSSO.integ.spec.ts @@ -0,0 +1,82 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromSSO } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromSSO.name, () => { + const ctest = new CTest({ + credentialProvider: fromSSO, + providerParams: (testParams) => { + return { + ssoStartUrl: "SSO_START_URL", + ssoAccountId: "1234567890", + ssoRegion: "sso-region-1", + ssoRoleName: "arn:aws:iam::1234567890:role/Rigmarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "unresolved", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + ctest.setIni({ + default: { + sso_start_url: "SSO_START_URL", + sso_account_id: "1234567890", + sso_region: "us-east-1", + sso_role_name: "Rigmarole", + }, + }); + const s3 = new S3({ + region: "us-east-1", + credentials: fromSSO({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_SSO_LEGACY: "u", + }, + accessKeyId: "SSO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: "SSO_SESSION_TOKEN_us-east-1", + }); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + credentials: fromSSO({ + ssoStartUrl: "SSO_START_URL", + ssoAccountId: "1234567890", + ssoRegion: "us-east-1", + ssoRoleName: "Rigmarole", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_SSO_LEGACY: "u", + }, + accessKeyId: "SSO_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "SSO_SECRET_ACCESS_KEY", + sessionToken: "SSO_SESSION_TOKEN_us-east-1", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts new file mode 100644 index 000000000000..a961c959b38c --- /dev/null +++ b/packages/credential-providers/tests/fromTemporaryCredentials.integ.spec.ts @@ -0,0 +1,62 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromTemporaryCredentials.name, () => { + const ctest = new CTest({ + credentialProvider: fromTemporaryCredentials, + providerParams: (testParams) => { + return { + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigmarole", + }, + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + ctest.setIni({ + alt: { + region: "us-east-2", + }, + }); + + const s3 = new S3({ + profile: "alt", + credentials: fromTemporaryCredentials({ + masterCredentials: { + accessKeyId: "M", + secretAccessKey: "M", + }, + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigmarole", + }, + }), + }); + + expect(await s3.config.credentials()).toMatchObject({ + sessionToken: "STS_AR_SESSION_TOKEN_us-east-2", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromTokenFile.integ.spec.ts b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts new file mode 100644 index 000000000000..e53b7f867882 --- /dev/null +++ b/packages/credential-providers/tests/fromTokenFile.integ.spec.ts @@ -0,0 +1,76 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromTokenFile } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromTokenFile.name, () => { + const ctest = new CTest({ + credentialProvider: fromTokenFile, + providerParams: (testParams) => { + return { + webIdentityTokenFile: "token-filepath", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is configurable from env", async () => { + process.env.AWS_WEB_IDENTITY_TOKEN_FILE = "token-filepath"; + process.env.AWS_ROLE_ARN = "arn:aws:iam::1234567890:role/Rigmarole"; + process.env.AWS_ROLE_SESSION_NAME = "role-session-1234"; + const s3 = new S3({ + region: "us-west-2", + credentials: fromTokenFile({}), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN: "h", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-west-2", + credentials: fromTokenFile({ + webIdentityTokenFile: "token-filepath", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + roleSessionName: "role-session-1234", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); +}); diff --git a/packages/credential-providers/tests/fromWebToken.integ.spec.ts b/packages/credential-providers/tests/fromWebToken.integ.spec.ts new file mode 100644 index 000000000000..a19d7516324b --- /dev/null +++ b/packages/credential-providers/tests/fromWebToken.integ.spec.ts @@ -0,0 +1,58 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { fromWebToken } from "@aws-sdk/credential-providers"; +import { describe, expect, test as it } from "vitest"; + +import { CTest } from "./_test-lib"; + +describe(fromWebToken.name, () => { + const ctest = new CTest({ + credentialProvider: fromWebToken, + providerParams: (testParams) => { + return { + webIdentityToken: "token-contents", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + ...CTest.defaultRegionConfigProvider(testParams), + }; + }, + profileCredentials: false, + fallbackRegion: "us-east-1", + }); + + ctest.testRegion(); + + describe("configure from env", () => { + it("is not configurable from env", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from profile", () => { + it("is not configurable from profile", async () => { + expect("ok").toBeTruthy(); + }); + }); + + describe("configure from code", () => { + it("should be configurable from code", async () => { + const s3 = new S3({ + region: "us-west-2", + credentials: fromWebToken({ + webIdentityToken: "token-contents", + roleArn: "arn:aws:iam::1234567890:role/Rigmarole", + roleSessionName: "role-session-1234", + }), + }); + await s3.listBuckets(); + expect(await s3.config.credentials()).toEqual({ + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_us-west-2", + }); + }); + }); +}); diff --git a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts index bb7e08de7d18..84970eaf6a24 100644 --- a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts +++ b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts @@ -2,6 +2,7 @@ // Please do not touch this file. It's generated from template in: // https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts import { setCredentialFeature } from "@aws-sdk/core/client"; +import { stsRegionDefaultResolver } from "@aws-sdk/region-config-resolver"; import type { CredentialProviderOptions } from "@aws-sdk/types"; import { AwsCredentialIdentity, Logger, Provider } from "@smithy/types"; @@ -28,8 +29,6 @@ export type RoleAssumer = ( params: AssumeRoleCommandInput ) => Promise; -const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; - interface AssumedRoleUser { /** * The ARN of the temporary security credentials that are returned from the AssumeRole action. @@ -63,19 +62,21 @@ const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { const resolveRegion = async ( _region: string | Provider | undefined, _parentRegion: string | Provider | undefined, - credentialProviderLogger?: Logger + credentialProviderLogger?: Logger, + loaderConfig: Parameters[0] = {} ): Promise => { const region: string | undefined = typeof _region === "function" ? await _region() : _region; const parentRegion: string | undefined = typeof _parentRegion === "function" ? await _parentRegion() : _parentRegion; + const stsDefaultRegion = await stsRegionDefaultResolver(loaderConfig)(); credentialProviderLogger?.debug?.( "@aws-sdk/client-sts::resolveRegion", "accepting first of:", - `${region} (provider)`, - `${parentRegion} (parent client)`, - `${ASSUME_ROLE_DEFAULT_REGION} (STS default)` + `${region} (credential provider clientConfig)`, + `${parentRegion} (contextual client)`, + `${stsDefaultRegion} (STS default: AWS_REGION, profile region, or us-east-1)` ); - return region ?? parentRegion ?? ASSUME_ROLE_DEFAULT_REGION; + return region ?? parentRegion ?? stsDefaultRegion; }; /** @@ -101,7 +102,11 @@ export const getDefaultRoleAssumer = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); @@ -164,7 +169,11 @@ export const getDefaultRoleAssumerWithWebIdentity = ( const resolvedRegion = await resolveRegion( region, stsOptions?.parentClientConfig?.region, - credentialProviderLogger + credentialProviderLogger, + { + logger, + profile, + } ); const isCompatibleRequestHandler = !isH2(requestHandler); diff --git a/packages/region-config-resolver/package.json b/packages/region-config-resolver/package.json index c4000efdc6d6..1ab0fba1aafc 100644 --- a/packages/region-config-resolver/package.json +++ b/packages/region-config-resolver/package.json @@ -25,6 +25,7 @@ "dependencies": { "@aws-sdk/types": "*", "@smithy/config-resolver": "^4.4.0", + "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, @@ -53,5 +54,9 @@ "type": "git", "url": "https://github.com/aws/aws-sdk-js-v3.git", "directory": "packages/region-config-resolver" - } + }, + "browser": { + "./dist-es/regionConfig/stsRegionDefaultResolver": "./dist-es/regionConfig/stsRegionDefaultResolver.browser" + }, + "react-native": {} } diff --git a/packages/region-config-resolver/src/index.ts b/packages/region-config-resolver/src/index.ts index f42620e237cf..d685b15dc9ee 100644 --- a/packages/region-config-resolver/src/index.ts +++ b/packages/region-config-resolver/src/index.ts @@ -1,2 +1,3 @@ export * from "./extensions"; export * from "./regionConfig/awsRegionConfig"; +export * from "./regionConfig/stsRegionDefaultResolver"; diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts new file mode 100644 index 000000000000..8a03c78cccee --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.browser.ts @@ -0,0 +1,6 @@ +/** + * @internal + */ +export function stsRegionDefaultResolver() { + return async () => "us-east-1"; +} diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts new file mode 100644 index 000000000000..8a03c78cccee --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.native.ts @@ -0,0 +1,6 @@ +/** + * @internal + */ +export function stsRegionDefaultResolver() { + return async () => "us-east-1"; +} diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts new file mode 100644 index 000000000000..433d1c533964 --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, test as it } from "vitest"; + +import { stsRegionDefaultResolver } from "./stsRegionDefaultResolver"; +import { stsRegionDefaultResolver as browser } from "./stsRegionDefaultResolver.browser"; +import { stsRegionDefaultResolver as native } from "./stsRegionDefaultResolver.native"; + +describe("stsRegionDefaultResolver", () => { + for (const impl of [stsRegionDefaultResolver, native, browser]) { + it(`should default to us-east-1`, async () => { + delete process.env.AWS_REGION; + const regionProvider = impl({ + profile: "non-existent P R O F I L E", + }); + const region = await regionProvider(); + expect(region).toBe("us-east-1"); + }); + } + + it("should use AWS_REGION before fallback to us-east-1", async () => { + process.env.AWS_REGION = "us-west-2"; + const regionProvider = stsRegionDefaultResolver({ + profile: "non-existent P R O F I L E", + }); + const region = await regionProvider(); + expect(region).toBe("us-west-2"); + }); +}); diff --git a/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts new file mode 100644 index 000000000000..52fb249f6ce9 --- /dev/null +++ b/packages/region-config-resolver/src/regionConfig/stsRegionDefaultResolver.ts @@ -0,0 +1,32 @@ +import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from "@smithy/config-resolver"; +import { type LocalConfigOptions, loadConfig } from "@smithy/node-config-provider"; + +/** + * Default region provider for STS when used as an inner client. + * Differs from the default region resolver in that us-east-1 is the fallback instead of throwing an error. + * + * @internal + */ +export function stsRegionDefaultResolver(loaderConfig: LocalConfigOptions = {}) { + return loadConfig( + { + ...NODE_REGION_CONFIG_OPTIONS, + async default() { + if (!warning.silence) { + console.warn( + "@aws-sdk - WARN - default STS region of us-east-1 used. See @aws-sdk/credential-providers README and set a region explicitly." + ); + } + return "us-east-1"; + }, + }, + { ...NODE_REGION_CONFIG_FILE_OPTIONS, ...loaderConfig } + ); +} + +/** + * @internal + */ +export const warning = { + silence: false, +}; diff --git a/packages/types/src/credentials.ts b/packages/types/src/credentials.ts index e7abfbf8c3e2..a9c175ff3252 100644 --- a/packages/types/src/credentials.ts +++ b/packages/types/src/credentials.ts @@ -49,6 +49,7 @@ export type CredentialProviderOptions = { parentClientConfig?: { region?: string | Provider; profile?: string; + logger?: Logger; [key: string]: unknown; }; }; diff --git a/yarn.lock b/yarn.lock index 836e384b6cc0..6d0a99288c28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24034,6 +24034,7 @@ __metadata: dependencies: "@aws-sdk/types": "npm:*" "@smithy/config-resolver": "npm:^4.4.0" + "@smithy/node-config-provider": "npm:^4.3.3" "@smithy/types": "npm:^4.8.0" "@tsconfig/recommended": "npm:1.0.1" concurrently: "npm:7.0.0"