-
Notifications
You must be signed in to change notification settings - Fork 584
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(credential-providers): add custom credential chain helper (#6374)
* feat(credential-providers): add custom credential chain helper * feat(credential-providers): update example code in readme Co-authored-by: Trivikram Kamat <[email protected]> * feat(credential-providers): update docs and minimum duration validation * feat(credential-providers): rename credential chain file --------- Co-authored-by: Trivikram Kamat <[email protected]>
- Loading branch information
Showing
4 changed files
with
200 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
packages/credential-providers/src/createCredentialChain.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { ProviderError } from "@smithy/property-provider"; | ||
import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; | ||
|
||
import { createCredentialChain } from "./createCredentialChain"; | ||
|
||
describe(createCredentialChain.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 = createCredentialChain(); | ||
|
||
try { | ||
await credentialProvider(); | ||
} catch (e) { | ||
expect(e).toBeDefined(); | ||
} | ||
|
||
expect.assertions(1); | ||
}); | ||
|
||
it("should create a custom chain", async () => { | ||
const credentialProvider = createCredentialChain(async () => mockCredentials); | ||
|
||
const credentials = await credentialProvider(); | ||
|
||
expect(credentials).toEqual(mockCredentials); | ||
}); | ||
|
||
it("should resolve a successful provider function", async () => { | ||
const credentialProvider = createCredentialChain(failure, failure, async () => mockCredentials, failure); | ||
|
||
const credentials = await credentialProvider(); | ||
|
||
expect(credentials).toEqual(mockCredentials); | ||
}); | ||
|
||
it("should resolve the first successful provider function", async () => { | ||
const credentialProvider = createCredentialChain( | ||
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 = createCredentialChain( | ||
failure, | ||
failure, | ||
async () => ({ ...mockCredentials, order: "1st" }), | ||
failure, | ||
async () => ({ ...mockCredentials, order: "2nd" }) | ||
).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() + 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." | ||
); | ||
}); | ||
}); |
77 changes: 77 additions & 0 deletions
77
packages/credential-providers/src/createCredentialChain.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
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<Type> = { | ||
-readonly [Property in keyof Type]: Type[Property]; | ||
}; | ||
|
||
/** | ||
* @example | ||
* ```js | ||
* 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: 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 | ||
* // when the time left is less than 5 minutes. | ||
* new S3({ | ||
* // expire after 15 minutes (in milliseconds). | ||
* credentials: createCredentialChain(fromEnv(), fromIni()).expireAfter(15 * 60_000), | ||
* }); | ||
* | ||
* // Apply shared init properties. | ||
* const init = { logger: console }; | ||
* | ||
* new S3({ | ||
* 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 createCredentialChain = ( | ||
...credentialProviders: AwsCredentialIdentityProvider[] | ||
): AwsCredentialIdentityProvider & CustomCredentialChainOptions => { | ||
let expireAfter = -1; | ||
const baseFunction = async () => { | ||
const credentials = await propertyProviderChain(...credentialProviders)(); | ||
if (!credentials.expiration && expireAfter !== -1) { | ||
(credentials as Mutable<typeof credentials>).expiration = new Date(Date.now() + expireAfter); | ||
} | ||
return credentials; | ||
}; | ||
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; | ||
}, | ||
}); | ||
return withOptions; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters