From a5a95d8be96033a511adf7de3610d7a2a2e96e0b Mon Sep 17 00:00:00 2001 From: George Fu Date: Wed, 11 Dec 2024 18:51:56 +0000 Subject: [PATCH] test(credential-provider-node): additional integ tests for cognito --- .../src/fromCognitoIdentityPool.ts | 6 +- packages/credential-provider-ini/package.json | 4 +- .../src/fromIni.integ.spec.ts | 242 ++++++++++++++++++ .../credential-provider-ini/src/fromIni.ts | 10 +- .../src/resolveAssumeRoleCredentials.ts | 26 +- .../vitest.config.integ.ts | 8 + .../credential-provider-node.integ.spec.ts | 158 +++++++++--- .../src/fromWebToken.ts | 12 +- packages/token-providers/src/fromSso.ts | 12 +- 9 files changed, 418 insertions(+), 60 deletions(-) create mode 100644 packages/credential-provider-ini/src/fromIni.integ.spec.ts create mode 100644 packages/credential-provider-ini/vitest.config.integ.ts diff --git a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts index 319e1463fff8..061b02dc45f3 100644 --- a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts +++ b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts @@ -68,11 +68,11 @@ export function fromCognitoIdentityPool({ identityId, }); - return provider(); + return provider(awsIdentityProperties); }; - return () => - provider().catch(async (err) => { + return (awsIdentityProperties?: AwsIdentityProperties) => + provider(awsIdentityProperties).catch(async (err) => { if (cacheKey) { Promise.resolve(cache.removeItem(cacheKey)).catch(() => {}); } diff --git a/packages/credential-provider-ini/package.json b/packages/credential-provider-ini/package.json index eb8aaf1521c3..d77e26b1e72d 100644 --- a/packages/credential-provider-ini/package.json +++ b/packages/credential-provider-ini/package.json @@ -13,7 +13,9 @@ "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo", "test": "yarn g:vitest run", - "test:watch": "yarn g:vitest watch" + "test:watch": "yarn g:vitest watch", + "test:integration": "yarn g:vitest run -c vitest.config.integ.ts", + "test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.ts" }, "keywords": [ "aws", diff --git a/packages/credential-provider-ini/src/fromIni.integ.spec.ts b/packages/credential-provider-ini/src/fromIni.integ.spec.ts new file mode 100644 index 000000000000..0cb0e93c00bd --- /dev/null +++ b/packages/credential-provider-ini/src/fromIni.integ.spec.ts @@ -0,0 +1,242 @@ +import { STS } from "@aws-sdk/client-sts"; +import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; +import { SourceProfileInit } from "@smithy/shared-ini-file-loader"; +import type { NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; +import { PassThrough } from "node:stream"; +import { beforeEach, describe, expect, test as it, vi } 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, + }; +}); + +class MockNodeHttpHandler { + static create(instanceOrOptions?: any) { + if (typeof instanceOrOptions?.handle === "function") { + return instanceOrOptions; + } + return new MockNodeHttpHandler(); + } + async handle(request: HttpRequest) { + const body = new PassThrough({}); + + const region = (request.hostname.match(/sts\.(.*?)\./) || [, "unknown"])[1]; + + 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", + 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 { + 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; + } +} + +describe("fromIni region search order", () => { + beforeEach(() => { + iniProfileData = { + default: { + region: "us-west-2", + output: "json", + }, + }; + 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", + }); + }); + + it("should use 1st priority for the clientConfig given to the provider factory", async () => { + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + region: "ap-northeast-2", + credentials: fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + region: "ap-northeast-1", + }, + }), + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toContain({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-1", + }); + }); + + it("should use 2nd priority for the context client", async () => { + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + region: "ap-northeast-2", + credentials: fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + }), + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toContain({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_ap-northeast-2", + }); + }); + + it("should use 3rd priority for the profile region if not used in the context of a client with a region", async () => { + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + region: "ap-northeast-2", + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toContain({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-stsar-1", + }); + }); + + it("should use 4th priority for the default partition's default region", async () => { + delete iniProfileData.default.region; + + const credentialsData = await fromIni({ + clientConfig: { + requestHandler: new MockNodeHttpHandler(), + }, + })(); + + const sts = new STS({ + requestHandler: new MockNodeHttpHandler(), + region: "ap-northeast-2", + credentials: credentialsData, + }); + + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toContain({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_us-east-1", + }); + }); +}); diff --git a/packages/credential-provider-ini/src/fromIni.ts b/packages/credential-provider-ini/src/fromIni.ts index 58e001f24347..08a8c2ae18cf 100644 --- a/packages/credential-provider-ini/src/fromIni.ts +++ b/packages/credential-provider-ini/src/fromIni.ts @@ -60,11 +60,13 @@ export const fromIni = async (props = {}) => { const init: FromIniInit = { ..._init, - parentClientConfig: { - region: props.contextClientConfig?.region, - ..._init.parentClientConfig, - }, }; + if (props.contextClientConfig?.region) { + init.parentClientConfig = { + region: props.contextClientConfig.region, + ..._init.parentClientConfig, + }; + } init.logger?.debug("@aws-sdk/credential-provider-ini - fromIni"); const profiles = await parseKnownFiles(init); return resolveProfileData(getProfileName(init), profiles, init); diff --git a/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts index a7bed9952747..1e9b7942555a 100644 --- a/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts +++ b/packages/credential-provider-ini/src/resolveAssumeRoleCredentials.ts @@ -107,7 +107,8 @@ export const resolveAssumeRoleCredentials = async ( visitedProfiles: Record = {} ) => { options.logger?.debug("@aws-sdk/credential-provider-ini - resolveAssumeRoleCredentials (STS)"); - const data = profiles[profileName]; + const profileData = profiles[profileName]; + const { source_profile, region } = profileData; if (!options.roleAssumer) { // @ts-ignore Cannot find module '@aws-sdk/client-sts' @@ -116,13 +117,18 @@ export const resolveAssumeRoleCredentials = async ( { ...options.clientConfig, credentialProviderLogger: options.logger, - parentClientConfig: options?.parentClientConfig, + parentClientConfig: { + ...options?.parentClientConfig, + // The profile region is the last fallback, and only applies + // if the clientConfig.region is not defined by the user + // and no contextual outer client configuration region can be found. + region: options?.parentClientConfig?.region ?? region, + }, }, options.clientPlugins ); } - const { source_profile } = data; if (source_profile && source_profile in visitedProfiles) { throw new CredentialsProviderError( `Detected a cycle attempting to resolve credentials for profile` + @@ -149,9 +155,9 @@ export const resolveAssumeRoleCredentials = async ( }, isCredentialSourceWithoutRoleArn(profiles[source_profile!] ?? {}) ) - : (await resolveCredentialSource(data.credential_source!, profileName, options.logger)(options))(); + : (await resolveCredentialSource(profileData.credential_source!, profileName, options.logger)(options))(); - if (isCredentialSourceWithoutRoleArn(data)) { + if (isCredentialSourceWithoutRoleArn(profileData)) { /** * This control-flow branch is accessed when in a chained source_profile * scenario, and the last step of the chain is a credential_source @@ -163,13 +169,13 @@ export const resolveAssumeRoleCredentials = async ( return sourceCredsProvider.then((creds) => setCredentialFeature(creds, "CREDENTIALS_PROFILE_SOURCE_PROFILE", "o")); } else { const params: AssumeRoleParams = { - RoleArn: data.role_arn!, - RoleSessionName: data.role_session_name || `aws-sdk-js-${Date.now()}`, - ExternalId: data.external_id, - DurationSeconds: parseInt(data.duration_seconds || "3600", 10), + RoleArn: profileData.role_arn!, + RoleSessionName: profileData.role_session_name || `aws-sdk-js-${Date.now()}`, + ExternalId: profileData.external_id, + DurationSeconds: parseInt(profileData.duration_seconds || "3600", 10), }; - const { mfa_serial } = data; + const { mfa_serial } = profileData; if (mfa_serial) { if (!options.mfaCodeProvider) { throw new CredentialsProviderError( diff --git a/packages/credential-provider-ini/vitest.config.integ.ts b/packages/credential-provider-ini/vitest.config.integ.ts new file mode 100644 index 000000000000..5802db1ac64a --- /dev/null +++ b/packages/credential-provider-ini/vitest.config.integ.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.integ.spec.ts"], + environment: "node", + }, +}); diff --git a/packages/credential-provider-node/src/credential-provider-node.integ.spec.ts b/packages/credential-provider-node/src/credential-provider-node.integ.spec.ts index f377bafacb38..b57651770bb9 100644 --- a/packages/credential-provider-node/src/credential-provider-node.integ.spec.ts +++ b/packages/credential-provider-node/src/credential-provider-node.integ.spec.ts @@ -1,6 +1,7 @@ import { STS } from "@aws-sdk/client-sts"; import * as credentialProviderHttp from "@aws-sdk/credential-provider-http"; -import { fromIni } from "@aws-sdk/credential-providers"; +import { fromCognitoIdentity, fromCognitoIdentityPool, fromIni, fromWebToken } from "@aws-sdk/credential-providers"; +import { fromSso } from "@aws-sdk/token-providers"; import { HttpResponse } from "@smithy/protocol-http"; import type { SourceProfileInit } from "@smithy/shared-ini-file-loader"; import type { HttpRequest, NodeHttpHandlerOptions, ParsedIniData } from "@smithy/types"; @@ -68,7 +69,7 @@ jest.mock("@smithy/node-http-handler", () => { assumeRoleArns.push(request.body.match(/RoleArn=(.*?)&/)?.[1]); } - const region = (request.hostname.match(/sts\.(.*?)\./) || [, "unknown"])[1]; + const region = (request.hostname.match(/(sts|cognito-identity)\.(.*?)\./) || [, , "unknown"])[2]; if (request.headers.Authorization === "container-authorization") { body.write( @@ -132,7 +133,22 @@ jest.mock("@smithy/node-http-handler", () => { 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(); @@ -451,38 +467,6 @@ describe("credential-provider-node integration test", () => { }); }); - it("should use the context client's region for STS even if initialized separately in a code-level provider", async () => { - sts = new STS({ - region: "eu-west-1", - credentials: fromIni(), - }); - iniProfileData.assume = { - region: "eu-west-1", - aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", - aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", - }; - Object.assign(iniProfileData.default, { - region: "eu-west-1", - role_arn: "ROLE_ARN", - role_session_name: "ROLE_SESSION_NAME", - external_id: "EXTERNAL_ID", - source_profile: "assume", - }); - await sts.getCallerIdentity({}); - const credentials = await sts.config.credentials(); - expect(credentials).toEqual({ - accessKeyId: "STS_AR_ACCESS_KEY_ID", - secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", - sessionToken: "STS_AR_SESSION_TOKEN_eu-west-1", - expiration: new Date("3000-01-01T00:00:00.000Z"), - $source: { - CREDENTIALS_CODE: "e", - CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", - CREDENTIALS_STS_ASSUME_ROLE: "i", - }, - }); - }); - it("should resolve credentials from STS assumeRoleWithWebIdentity if the ini profile is configured for web identity", async () => { Object.assign(iniProfileData.default, { web_identity_token_file: "token-filepath", @@ -820,6 +804,112 @@ describe("credential-provider-node integration test", () => { }); }); + describe("Region resolution for code-level providers given to a client", () => { + it("fromCognitoIdentity provider should use context client region", async () => { + sts = new STS({ + region: "ap-northeast-1", + credentials: fromCognitoIdentity({ + identityId: "", + }), + }); + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toEqual({ + accessKeyId: "COGNITO_ACCESS_KEY_ID", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_ap-northeast-1", + identityId: "", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { CREDENTIALS_CODE: "e" }, + }); + }); + + it("fromCognitoIdentityPool provider should use context client region", async () => { + sts = new STS({ + region: "ap-northeast-1", + credentials: fromCognitoIdentityPool({ + identityPoolId: "", + }), + }); + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toEqual({ + accessKeyId: "COGNITO_ACCESS_KEY_ID", + secretAccessKey: "COGNITO_SECRET_KEY", + sessionToken: "COGNITO_SESSION_TOKEN_ap-northeast-1", + identityId: "ap-northeast-1:COGNITO_IDENTITY_ID", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { CREDENTIALS_CODE: "e" }, + }); + }); + + it("fromIni assumeRole provider should use the context client's region for STS", async () => { + sts = new STS({ + region: "eu-west-1", + credentials: fromIni(), + }); + iniProfileData.assume = { + region: "eu-west-1", + aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY", + aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY", + }; + Object.assign(iniProfileData.default, { + region: "eu-west-1", + role_arn: "ROLE_ARN", + role_session_name: "ROLE_SESSION_NAME", + external_id: "EXTERNAL_ID", + source_profile: "assume", + }); + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toEqual({ + accessKeyId: "STS_AR_ACCESS_KEY_ID", + secretAccessKey: "STS_AR_SECRET_ACCESS_KEY", + sessionToken: "STS_AR_SESSION_TOKEN_eu-west-1", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o", + CREDENTIALS_STS_ASSUME_ROLE: "i", + }, + }); + }); + + it("fromWebToken provider should use context client region", async () => { + sts = new STS({ + region: "ap-northeast-1", + credentials: fromWebToken({ + roleArn: "", + webIdentityToken: "", + }), + }); + await sts.getCallerIdentity({}); + const credentials = await sts.config.credentials(); + expect(credentials).toEqual({ + accessKeyId: "STS_ARWI_ACCESS_KEY_ID", + secretAccessKey: "STS_ARWI_SECRET_ACCESS_KEY", + sessionToken: "STS_ARWI_SESSION_TOKEN_ap-northeast-1", + expiration: new Date("3000-01-01T00:00:00.000Z"), + $source: { + CREDENTIALS_CODE: "e", + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k", + }, + }); + }); + + it.skip( + "fromSSO (SSO) provider is excluded from testing because the SSO_REGION is a required parameter and is used " + + "instead of any fallback to the context client region", + async () => {} + ); + + it.skip( + "fromSso (SSO-OIDC) provider is excluded from testing because it is " + + "not used in a client initialization context", + async () => {} + ); + }); + describe("No credentials available", () => { it("should throw CredentialsProviderError", async () => { process.env.AWS_EC2_METADATA_DISABLED = "true"; diff --git a/packages/credential-provider-web-identity/src/fromWebToken.ts b/packages/credential-provider-web-identity/src/fromWebToken.ts index 5e16e863fd96..880a27772b3c 100644 --- a/packages/credential-provider-web-identity/src/fromWebToken.ts +++ b/packages/credential-provider-web-identity/src/fromWebToken.ts @@ -169,10 +169,14 @@ export const fromWebToken = { ...init.clientConfig, credentialProviderLogger: init.logger, - parentClientConfig: { - region: awsIdentityProperties?.contextClientConfig?.region, - ...init.parentClientConfig, - }, + ...(awsIdentityProperties?.contextClientConfig?.region + ? { + parentClientConfig: { + region: awsIdentityProperties?.contextClientConfig?.region, + ...init.parentClientConfig, + }, + } + : {}), }, init.clientPlugins ); diff --git a/packages/token-providers/src/fromSso.ts b/packages/token-providers/src/fromSso.ts index dbee38ce5e44..516b4efea1ca 100644 --- a/packages/token-providers/src/fromSso.ts +++ b/packages/token-providers/src/fromSso.ts @@ -40,10 +40,14 @@ export const fromSso = async (awsIdentityProperties?: AwsIdentityProperties) => { const init: FromSsoInit = { ..._init, - parentClientConfig: { - region: awsIdentityProperties?.contextClientConfig?.region, - ..._init.parentClientConfig, - }, + ...(awsIdentityProperties?.contextClientConfig?.region + ? { + parentClientConfig: { + region: awsIdentityProperties?.contextClientConfig?.region, + ..._init.parentClientConfig, + }, + } + : {}), }; init.logger?.debug("@aws-sdk/token-providers - fromSso");