From 150503012beaad80ae77b29f32691a5db49c359b Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 9 Aug 2024 20:38:05 +0000 Subject: [PATCH 1/4] feat(credential-providers): add custom credential chain helper --- packages/credential-providers/README.md | 36 ++++++++- .../src/customCredentialChain.spec.ts | 73 +++++++++++++++++++ .../src/customCredentialChain.ts | 69 ++++++++++++++++++ packages/credential-providers/src/index.ts | 3 +- 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 packages/credential-providers/src/customCredentialChain.spec.ts create mode 100644 packages/credential-providers/src/customCredentialChain.ts diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index a5dc718f9f776..1cebec7f81370 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -25,6 +25,7 @@ A collection of all credential providers, with default clients. 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](#chain) ## `fromCognitoIdentity()` @@ -704,14 +705,14 @@ CLI profile name [123456789011_ReadOnly]: my-sso-profile ```javascript //... -const client = new FooClient({ credentials: fromSSO({ profile: "my-sso-profile" })}); +const client = new FooClient({ credentials: fromSSO({ profile: "my-sso-profile" }) }); ``` Alternatively, the SSO credential provider is supported in shared INI credentials provider ```javascript //... -const client = new FooClient({ credentials: fromIni({ profile: "my-sso-profile" })}); +const client = new FooClient({ credentials: fromIni({ profile: "my-sso-profile" }) }); ``` 3. To log out from the current SSO session, use the AWS CLI: @@ -784,6 +785,37 @@ const credentialProvider = fromNodeProviderChain({ }); ``` +## `chain()` + +You can use this helper to create a credential chain of your own. + +```ts +import { fromEnv, fromIni, chain } from "@aws-sdk/credential-providers"; +import { S3 } from "@aws-sdk/client-s3"; + +// You can mix existing AWS SDK credential providers +// and custom async functions returning credential objects. +new S3({ + credentials: chain(fromEnv(), fromIni(), async () => { + return { + ...myCredentialsFromSomewhereElse, + }; + }), +}); + +// Set a max duration on the credentials (client side only). +// A set expiration will cause the credentials function to be called again +// after the given duration. +new S3({ + credentials: chain(fromEnv(), fromIni()).expireAfter(15 * 60_000), // 15 minutes in milliseconds. +}); + +// apply shared init properties. +new S3({ + credentials: chain(...[fromEnv, fromIni].map((p) => p({ logger: console }))), +}); +``` + ## Add Custom Headers to STS assume-role calls You can specify the plugins--groups of middleware, to inject to the STS client. diff --git a/packages/credential-providers/src/customCredentialChain.spec.ts b/packages/credential-providers/src/customCredentialChain.spec.ts new file mode 100644 index 0000000000000..9a45e3c629efe --- /dev/null +++ b/packages/credential-providers/src/customCredentialChain.spec.ts @@ -0,0 +1,73 @@ +import { ProviderError } from "@smithy/property-provider"; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; + +import { chain } from "./customCredentialChain"; + +describe(chain.name, () => { + const mockCredentials: AwsCredentialIdentity = { + accessKeyId: "AKI", + secretAccessKey: "SAK", + }; + + const failure = async () => { + throw new ProviderError("", { tryNextLink: true }); + }; + + it("should throw an error if zero providers are chained", async () => { + const credentialProvider = chain(); + + try { + await credentialProvider(); + } catch (e) { + expect(e).toBeDefined(); + } + + expect.assertions(1); + }); + + it("should create a custom chain", async () => { + const credentialProvider = chain(async () => mockCredentials); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve a successful provider function", async () => { + const credentialProvider = chain(failure, failure, async () => mockCredentials, failure); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve the first successful provider function", async () => { + const credentialProvider = chain( + failure, + failure, + async () => ({ ...mockCredentials, order: "1st" }), + failure, + async () => ({ ...mockCredentials, order: "2nd" }) + ); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual({ ...mockCredentials, order: "1st" }); + }); + + it("should allow setting a duration", async () => { + const credentialProvider: AwsCredentialIdentityProvider = chain( + failure, + failure, + async () => ({ ...mockCredentials, order: "1st" }), + failure, + async () => ({ ...mockCredentials, order: "2nd" }) + ).expireAfter(15_000); + + const credentials = await credentialProvider(); + + expect(credentials.expiration).toBeDefined(); + expect(credentials.expiration?.getTime()).toBeGreaterThan(Date.now()); + expect(credentials.expiration?.getTime()).toBeLessThan(Date.now() + 30_000); + }); +}); diff --git a/packages/credential-providers/src/customCredentialChain.ts b/packages/credential-providers/src/customCredentialChain.ts new file mode 100644 index 0000000000000..32915351080aa --- /dev/null +++ b/packages/credential-providers/src/customCredentialChain.ts @@ -0,0 +1,69 @@ +import { chain as propertyProviderChain } from "@smithy/property-provider"; +import type { AwsCredentialIdentityProvider } from "@smithy/types"; + +export interface CustomCredentialChainOptions { + expireAfter(milliseconds: number): AwsCredentialIdentityProvider & CustomCredentialChainOptions; +} + +/** + * @internal + */ +type Mutable = { + -readonly [Property in keyof Type]: Type[Property]; +}; + +/** + * @example + * ```js + * import { fromEnv, fromIni, chain } from "@aws-sdk/credential-providers"; + * import { S3 } from '@aws-sdk/client-s3'; + * + * // basic chain. + * new S3({ + * credentials: chain( + * fromEnv(), + * fromIni() + * ) + * }); + * + * // set a max duration on the credentials (client side only). + * new S3({ + * credentials: chain( + * fromEnv(), + * fromIni() + * ).expireAfter(15 * 60_000) // 15 minutes in milliseconds. + * }); + * + * // apply shared init properties. + * new S3({ + * credentials: chain(...[ + * fromEnv, + * fromIni + * ].map(p => p({ logger: console }))) + * }); + * + * ``` + * + * @param credentialProviders - one or more credential providers. + * @returns a single AwsCredentialIdentityProvider that calls the given + * providers in sequence until one succeeds or all fail. + */ +export const chain = ( + ...credentialProviders: AwsCredentialIdentityProvider[] +): AwsCredentialIdentityProvider & CustomCredentialChainOptions => { + let expireAfter = -1; + const baseFunction = async () => { + const credentials = await propertyProviderChain(...credentialProviders)(); + if (!credentials.expiration && expireAfter !== -1) { + (credentials as Mutable).expiration = new Date(Date.now() + expireAfter); + } + return credentials; + }; + const withOptions = Object.assign(baseFunction, { + expireAfter(milliseconds: number) { + expireAfter = milliseconds; + return withOptions; + }, + }); + return withOptions; +}; diff --git a/packages/credential-providers/src/index.ts b/packages/credential-providers/src/index.ts index a1e419ae6188c..1845e3413d68d 100644 --- a/packages/credential-providers/src/index.ts +++ b/packages/credential-providers/src/index.ts @@ -1,8 +1,9 @@ +export * from "./customCredentialChain"; export * from "./fromCognitoIdentity"; export * from "./fromCognitoIdentityPool"; export * from "./fromContainerMetadata"; -export * from "./fromEnv"; export { fromHttp, FromHttpOptions, HttpProviderCredentials } from "@aws-sdk/credential-provider-http"; +export * from "./fromEnv"; export * from "./fromIni"; export * from "./fromInstanceMetadata"; export * from "./fromNodeProviderChain"; From bfa9b4ed230c3f4f5a02aa3b6a872f8e1a690abb Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 12 Aug 2024 11:22:42 -0400 Subject: [PATCH 2/4] feat(credential-providers): update example code in readme Co-authored-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> --- packages/credential-providers/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index 1cebec7f81370..5846389323519 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -797,9 +797,7 @@ import { S3 } from "@aws-sdk/client-s3"; // and custom async functions returning credential objects. new S3({ credentials: chain(fromEnv(), fromIni(), async () => { - return { - ...myCredentialsFromSomewhereElse, - }; + return myCredentialsFromSomewhereElse; }), }); From f19145796db44a15034794cb3c796cf064ee5afb Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 12 Aug 2024 19:51:55 +0000 Subject: [PATCH 3/4] feat(credential-providers): update docs and minimum duration validation --- packages/credential-providers/README.md | 24 +++++++---- .../src/customCredentialChain.spec.ts | 26 +++++++----- .../src/customCredentialChain.ts | 40 +++++++++++-------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index 5846389323519..3211a8f116270 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -790,27 +790,35 @@ const credentialProvider = fromNodeProviderChain({ You can use this helper to create a credential chain of your own. ```ts -import { fromEnv, fromIni, chain } from "@aws-sdk/credential-providers"; +import { fromEnv, fromIni, createCredentialChain } from "@aws-sdk/credential-providers"; import { S3 } from "@aws-sdk/client-s3"; // You can mix existing AWS SDK credential providers // and custom async functions returning credential objects. new S3({ - credentials: chain(fromEnv(), fromIni(), async () => { - return myCredentialsFromSomewhereElse; - }), + credentials: createCredentialChain( + fromEnv(), + async () => { + // credentials customized by your code... + return credentials; + }, + fromIni() + ), }); // Set a max duration on the credentials (client side only). // A set expiration will cause the credentials function to be called again -// after the given duration. +// when the time left is less than 5 minutes. new S3({ - credentials: chain(fromEnv(), fromIni()).expireAfter(15 * 60_000), // 15 minutes in milliseconds. + // expire after 15 minutes (in milliseconds). + credentials: createCredentialChain(fromEnv(), fromIni()).expireAfter(15 * 60_000), }); -// apply shared init properties. +// Apply shared init properties. +const init = { logger: console }; + new S3({ - credentials: chain(...[fromEnv, fromIni].map((p) => p({ logger: console }))), + credentials: createCredentialChain(fromEnv(init), fromIni(init)), }); ``` diff --git a/packages/credential-providers/src/customCredentialChain.spec.ts b/packages/credential-providers/src/customCredentialChain.spec.ts index 9a45e3c629efe..425fb7c2c5b13 100644 --- a/packages/credential-providers/src/customCredentialChain.spec.ts +++ b/packages/credential-providers/src/customCredentialChain.spec.ts @@ -1,9 +1,9 @@ import { ProviderError } from "@smithy/property-provider"; import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; -import { chain } from "./customCredentialChain"; +import { createCredentialChain } from "./customCredentialChain"; -describe(chain.name, () => { +describe(createCredentialChain.name, () => { const mockCredentials: AwsCredentialIdentity = { accessKeyId: "AKI", secretAccessKey: "SAK", @@ -14,7 +14,7 @@ describe(chain.name, () => { }; it("should throw an error if zero providers are chained", async () => { - const credentialProvider = chain(); + const credentialProvider = createCredentialChain(); try { await credentialProvider(); @@ -26,7 +26,7 @@ describe(chain.name, () => { }); it("should create a custom chain", async () => { - const credentialProvider = chain(async () => mockCredentials); + const credentialProvider = createCredentialChain(async () => mockCredentials); const credentials = await credentialProvider(); @@ -34,7 +34,7 @@ describe(chain.name, () => { }); it("should resolve a successful provider function", async () => { - const credentialProvider = chain(failure, failure, async () => mockCredentials, failure); + const credentialProvider = createCredentialChain(failure, failure, async () => mockCredentials, failure); const credentials = await credentialProvider(); @@ -42,7 +42,7 @@ describe(chain.name, () => { }); it("should resolve the first successful provider function", async () => { - const credentialProvider = chain( + const credentialProvider = createCredentialChain( failure, failure, async () => ({ ...mockCredentials, order: "1st" }), @@ -56,18 +56,26 @@ describe(chain.name, () => { }); it("should allow setting a duration", async () => { - const credentialProvider: AwsCredentialIdentityProvider = chain( + const credentialProvider: AwsCredentialIdentityProvider = createCredentialChain( failure, failure, async () => ({ ...mockCredentials, order: "1st" }), failure, async () => ({ ...mockCredentials, order: "2nd" }) - ).expireAfter(15_000); + ).expireAfter(6 * 60_000); const credentials = await credentialProvider(); expect(credentials.expiration).toBeDefined(); expect(credentials.expiration?.getTime()).toBeGreaterThan(Date.now()); - expect(credentials.expiration?.getTime()).toBeLessThan(Date.now() + 30_000); + expect(credentials.expiration?.getTime()).toBeLessThan(Date.now() + 375_000); + }); + + it("it should throw an error for durations less than 5 minutes", async () => { + expect(() => { + createCredentialChain(async () => mockCredentials).expireAfter(299_999); + }).toThrow( + "@aws-sdk/credential-providers - createCredentialChain(...).expireAfter(ms) may not be called with a duration lower than five minutes." + ); }); }); diff --git a/packages/credential-providers/src/customCredentialChain.ts b/packages/credential-providers/src/customCredentialChain.ts index 32915351080aa..857ce1b5345a4 100644 --- a/packages/credential-providers/src/customCredentialChain.ts +++ b/packages/credential-providers/src/customCredentialChain.ts @@ -15,40 +15,43 @@ type Mutable = { /** * @example * ```js - * import { fromEnv, fromIni, chain } from "@aws-sdk/credential-providers"; + * import { fromEnv, fromIni, createCredentialChain } from '@aws-sdk/credential-providers'; * import { S3 } from '@aws-sdk/client-s3'; * - * // basic chain. + * // You can mix existing AWS SDK credential providers + * // and custom async functions returning credential objects. * new S3({ - * credentials: chain( + * credentials: createCredentialChain( * fromEnv(), + * async () => { + * // credentials customized by your code... + * return credentials; + * }, * fromIni() - * ) + * ), * }); * - * // set a max duration on the credentials (client side only). + * // Set a max duration on the credentials (client side only). + * // A set expiration will cause the credentials function to be called again + * // when the time left is less than 5 minutes. * new S3({ - * credentials: chain( - * fromEnv(), - * fromIni() - * ).expireAfter(15 * 60_000) // 15 minutes in milliseconds. + * // expire after 15 minutes (in milliseconds). + * credentials: createCredentialChain(fromEnv(), fromIni()).expireAfter(15 * 60_000), * }); * - * // apply shared init properties. + * // Apply shared init properties. + * const init = { logger: console }; + * * new S3({ - * credentials: chain(...[ - * fromEnv, - * fromIni - * ].map(p => p({ logger: console }))) + * credentials: createCredentialChain(fromEnv(init), fromIni(init)), * }); - * * ``` * * @param credentialProviders - one or more credential providers. * @returns a single AwsCredentialIdentityProvider that calls the given * providers in sequence until one succeeds or all fail. */ -export const chain = ( +export const createCredentialChain = ( ...credentialProviders: AwsCredentialIdentityProvider[] ): AwsCredentialIdentityProvider & CustomCredentialChainOptions => { let expireAfter = -1; @@ -61,6 +64,11 @@ export const chain = ( }; const withOptions = Object.assign(baseFunction, { expireAfter(milliseconds: number) { + if (milliseconds < 5 * 60_000) { + throw new Error( + "@aws-sdk/credential-providers - createCredentialChain(...).expireAfter(ms) may not be called with a duration lower than five minutes." + ); + } expireAfter = milliseconds; return withOptions; }, From bcedb5a22c799e0c3cf9c55505ad4295e6bcbfd0 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 12 Aug 2024 20:00:54 +0000 Subject: [PATCH 4/4] feat(credential-providers): rename credential chain file --- ...tomCredentialChain.spec.ts => createCredentialChain.spec.ts} | 2 +- .../src/{customCredentialChain.ts => createCredentialChain.ts} | 0 packages/credential-providers/src/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/credential-providers/src/{customCredentialChain.spec.ts => createCredentialChain.spec.ts} (97%) rename packages/credential-providers/src/{customCredentialChain.ts => createCredentialChain.ts} (100%) diff --git a/packages/credential-providers/src/customCredentialChain.spec.ts b/packages/credential-providers/src/createCredentialChain.spec.ts similarity index 97% rename from packages/credential-providers/src/customCredentialChain.spec.ts rename to packages/credential-providers/src/createCredentialChain.spec.ts index 425fb7c2c5b13..911f41e7b44c2 100644 --- a/packages/credential-providers/src/customCredentialChain.spec.ts +++ b/packages/credential-providers/src/createCredentialChain.spec.ts @@ -1,7 +1,7 @@ import { ProviderError } from "@smithy/property-provider"; import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; -import { createCredentialChain } from "./customCredentialChain"; +import { createCredentialChain } from "./createCredentialChain"; describe(createCredentialChain.name, () => { const mockCredentials: AwsCredentialIdentity = { diff --git a/packages/credential-providers/src/customCredentialChain.ts b/packages/credential-providers/src/createCredentialChain.ts similarity index 100% rename from packages/credential-providers/src/customCredentialChain.ts rename to packages/credential-providers/src/createCredentialChain.ts diff --git a/packages/credential-providers/src/index.ts b/packages/credential-providers/src/index.ts index 1845e3413d68d..e23687adcc6f2 100644 --- a/packages/credential-providers/src/index.ts +++ b/packages/credential-providers/src/index.ts @@ -1,4 +1,4 @@ -export * from "./customCredentialChain"; +export * from "./createCredentialChain"; export * from "./fromCognitoIdentity"; export * from "./fromCognitoIdentityPool"; export * from "./fromContainerMetadata";