-
Notifications
You must be signed in to change notification settings - Fork 578
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
- Loading branch information
Showing
5 changed files
with
207 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
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
88 changes: 88 additions & 0 deletions
88
packages/credential-providers/src/customCredentialChain.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,88 @@ | ||
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 an expiration", async () => { | ||
const credentialProvider: AwsCredentialIdentityProvider = chain( | ||
failure, | ||
failure, | ||
async () => ({ ...mockCredentials, order: "1st" }), | ||
failure, | ||
async () => ({ ...mockCredentials, order: "2nd" }) | ||
).withExpiration(new Date("2024-08-09T19:53:27.900Z")); | ||
|
||
const credentials = await credentialProvider(); | ||
|
||
expect(credentials.expiration).toBeDefined(); | ||
expect(credentials.expiration?.getTime()).toEqual(1723233207900); | ||
}); | ||
|
||
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); | ||
}); | ||
}); |
77 changes: 77 additions & 0 deletions
77
packages/credential-providers/src/customCredentialChain.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 { | ||
withExpiration(expiration: Date): AwsCredentialIdentityProvider & CustomCredentialChainOptions; | ||
expireAfter(milliseconds: number): AwsCredentialIdentityProvider & CustomCredentialChainOptions; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
type Mutable<Type> = { | ||
-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 expiration: Date | undefined = undefined; | ||
let expireAfter = -1; | ||
const baseFunction = async () => { | ||
const credentials = await propertyProviderChain(...credentialProviders)(); | ||
if ((expiration?.getTime?.() ?? Infinity) < (credentials.expiration?.getTime?.() ?? Infinity)) { | ||
(credentials as Mutable<typeof credentials>).expiration = expiration; | ||
} else if (!credentials.expiration && expireAfter !== -1) { | ||
(credentials as Mutable<typeof credentials>).expiration = new Date(Date.now() + expireAfter); | ||
} | ||
return credentials; | ||
}; | ||
const withOptions = Object.assign(baseFunction, { | ||
withExpiration(_expiration: Date) { | ||
expiration = _expiration; | ||
return withOptions; | ||
}, | ||
expireAfter(milliseconds: number) { | ||
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