-
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-provider-node): refactor into modular components (#3294)
- Loading branch information
Showing
6 changed files
with
379 additions
and
709 deletions.
There are no files selected for viewing
208 changes: 208 additions & 0 deletions
208
packages/credential-provider-node/src/defaultProvider.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,208 @@ | ||
import { fromEnv } from "@aws-sdk/credential-provider-env"; | ||
import { RemoteProviderInit } from "@aws-sdk/credential-provider-imds"; | ||
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini"; | ||
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process"; | ||
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso"; | ||
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity"; | ||
import { chain, CredentialsProviderError, memoize } from "@aws-sdk/property-provider"; | ||
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader"; | ||
import { CredentialProvider } from "@aws-sdk/types"; | ||
import { ENV_PROFILE } from "@aws-sdk/util-credentials"; | ||
|
||
import { defaultProvider } from "./defaultProvider"; | ||
import { remoteProvider } from "./remoteProvider"; | ||
|
||
jest.mock("@aws-sdk/credential-provider-env"); | ||
jest.mock("@aws-sdk/credential-provider-imds"); | ||
jest.mock("@aws-sdk/credential-provider-ini"); | ||
jest.mock("@aws-sdk/credential-provider-process"); | ||
jest.mock("@aws-sdk/credential-provider-sso"); | ||
jest.mock("@aws-sdk/credential-provider-web-identity"); | ||
jest.mock("@aws-sdk/property-provider"); | ||
jest.mock("@aws-sdk/shared-ini-file-loader"); | ||
jest.mock("./remoteProvider"); | ||
|
||
describe(defaultProvider.name, () => { | ||
const mockCreds = { | ||
accessKeyId: "mockAccessKeyId", | ||
secretAccessKey: "mockSecretAccessKey", | ||
}; | ||
|
||
const mockInit = { | ||
profile: "mockProfile", | ||
loadedConfig: Promise.resolve({ configFile: {}, credentialsFile: {} }), | ||
}; | ||
|
||
const mockEnvFn = jest.fn(); | ||
const mockSsoFn = jest.fn(); | ||
const mockIniFn = jest.fn(); | ||
const mockProcessFn = jest.fn(); | ||
const mockTokenFileFn = jest.fn(); | ||
const mockRemoteProviderFn = jest.fn(); | ||
|
||
const mockChainFn = jest.fn(); | ||
const mockMemoizeFn = jest.fn().mockResolvedValue(mockCreds); | ||
|
||
beforeEach(() => { | ||
[ | ||
[fromEnv, mockEnvFn], | ||
[fromSSO, mockSsoFn], | ||
[fromIni, mockIniFn], | ||
[fromProcess, mockProcessFn], | ||
[fromTokenFile, mockTokenFileFn], | ||
[remoteProvider, mockRemoteProviderFn], | ||
[chain, mockChainFn], | ||
[memoize, mockMemoizeFn], | ||
].forEach(([fromFn, mockFn]) => { | ||
(fromFn as jest.Mock).mockReturnValue(mockFn); | ||
}); | ||
}); | ||
|
||
afterEach(async () => { | ||
const errorFnIndex = (chain as jest.Mock).mock.calls[0].length; | ||
const errorFn = (chain as jest.Mock).mock.calls[0][errorFnIndex - 1]; | ||
const expectedError = new CredentialsProviderError("Could not load credentials from any providers", false); | ||
try { | ||
await errorFn(); | ||
fail(`expected ${expectedError}`); | ||
} catch (error) { | ||
expect(error).toStrictEqual(expectedError); | ||
} | ||
|
||
expect(memoize).toHaveBeenCalledWith(mockChainFn, expect.any(Function), expect.any(Function)); | ||
|
||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe("without fromEnv", () => { | ||
afterEach(() => { | ||
expect(chain).toHaveBeenCalledWith( | ||
mockSsoFn, | ||
mockIniFn, | ||
mockProcessFn, | ||
mockTokenFileFn, | ||
mockRemoteProviderFn, | ||
expect.any(Function) | ||
); | ||
}); | ||
|
||
it("creates provider chain and memoizes it", async () => { | ||
const receivedCreds = await defaultProvider(mockInit)(); | ||
expect(receivedCreds).toStrictEqual(mockCreds); | ||
|
||
expect(fromEnv).not.toHaveBeenCalled(); | ||
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) { | ||
expect(fromFn).toHaveBeenCalledWith(mockInit); | ||
} | ||
|
||
expect(loadSharedConfigFiles).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it(`reads profile from env['${ENV_PROFILE}'], if not provided in init`, async () => { | ||
const ORIGINAL_ENV = process.env; | ||
process.env = { | ||
...ORIGINAL_ENV, | ||
[ENV_PROFILE]: "envProfile", | ||
}; | ||
|
||
const { profile, ...mockInitWithoutProfile } = mockInit; | ||
const receivedCreds = await defaultProvider(mockInitWithoutProfile)(); | ||
expect(receivedCreds).toStrictEqual(mockCreds); | ||
|
||
expect(fromEnv).not.toHaveBeenCalled(); | ||
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) { | ||
expect(fromFn).toHaveBeenCalledWith({ ...mockInit, profile: process.env[ENV_PROFILE] }); | ||
} | ||
|
||
process.env = ORIGINAL_ENV; | ||
}); | ||
|
||
it(`gets loadedConfig from loadSharedConfigFiles, if not provided in init`, async () => { | ||
const mockSharedConfigFiles = Promise.resolve({ | ||
configFile: { key: "value" }, | ||
credentialsFile: { key: "value" }, | ||
}); | ||
(loadSharedConfigFiles as jest.Mock).mockReturnValue(mockSharedConfigFiles); | ||
|
||
const { loadedConfig, ...mockInitWithoutLoadedConfig } = mockInit; | ||
const receivedCreds = await defaultProvider(mockInitWithoutLoadedConfig)(); | ||
expect(receivedCreds).toStrictEqual(mockCreds); | ||
|
||
expect(loadSharedConfigFiles).toHaveBeenCalledWith(mockInitWithoutLoadedConfig); | ||
|
||
expect(fromEnv).not.toHaveBeenCalled(); | ||
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) { | ||
expect(fromFn).toHaveBeenCalledWith({ ...mockInit, loadedConfig: mockSharedConfigFiles }); | ||
} | ||
}); | ||
}); | ||
|
||
it(`adds fromEnv call if profile is not available`, async () => { | ||
const { profile, ...mockInitWithoutProfile } = mockInit; | ||
const receivedCreds = await defaultProvider(mockInitWithoutProfile)(); | ||
expect(receivedCreds).toStrictEqual(mockCreds); | ||
|
||
expect(fromEnv).toHaveBeenCalledTimes(1); | ||
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) { | ||
expect(fromFn).toHaveBeenCalledWith(mockInitWithoutProfile); | ||
} | ||
|
||
expect(chain).toHaveBeenCalledWith( | ||
mockEnvFn, | ||
mockSsoFn, | ||
mockIniFn, | ||
mockProcessFn, | ||
mockTokenFileFn, | ||
mockRemoteProviderFn, | ||
expect.any(Function) | ||
); | ||
}); | ||
|
||
describe("memoize isExpired", () => { | ||
const mockDateNow = Date.now(); | ||
beforeEach(async () => { | ||
jest.spyOn(Date, "now").mockReturnValueOnce(mockDateNow); | ||
await defaultProvider(mockInit)(); | ||
}); | ||
|
||
it("returns true if expiration is defined, and creds have expired", () => { | ||
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1]; | ||
const expiration = new Date(mockDateNow - 24 * 60 * 60 * 1000); | ||
expect(memoizeExpiredFn({ expiration })).toEqual(true); | ||
}); | ||
|
||
it("returns true if expiration is defined, and creds expire in <5 mins", () => { | ||
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1]; | ||
const expiration = new Date(mockDateNow + 299 * 1000); | ||
expect(memoizeExpiredFn({ expiration })).toEqual(true); | ||
}); | ||
|
||
it("returns false if expiration is defined, but creds expire in >5 mins", () => { | ||
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1]; | ||
const expiration = new Date(mockDateNow + 301 * 1000); | ||
expect(memoizeExpiredFn({ expiration })).toEqual(false); | ||
}); | ||
|
||
it("returns false if expiration is not defined", () => { | ||
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1]; | ||
expect(memoizeExpiredFn({})).toEqual(false); | ||
}); | ||
}); | ||
|
||
describe("memoize requiresRefresh", () => { | ||
beforeEach(async () => { | ||
await defaultProvider(mockInit)(); | ||
}); | ||
|
||
it("returns true if expiration is not defined", () => { | ||
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2]; | ||
const expiration = Date.now(); | ||
expect(memoizeRefreshFn({ expiration })).toEqual(true); | ||
}); | ||
|
||
it("returns false if expiration is not defined", () => { | ||
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2]; | ||
expect(memoizeRefreshFn({})).toEqual(false); | ||
}); | ||
}); | ||
}); |
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,74 @@ | ||
import { fromEnv } from "@aws-sdk/credential-provider-env"; | ||
import { RemoteProviderInit } from "@aws-sdk/credential-provider-imds"; | ||
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini"; | ||
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process"; | ||
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso"; | ||
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity"; | ||
import { chain, CredentialsProviderError, memoize } from "@aws-sdk/property-provider"; | ||
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader"; | ||
import { CredentialProvider } from "@aws-sdk/types"; | ||
import { ENV_PROFILE } from "@aws-sdk/util-credentials"; | ||
|
||
import { remoteProvider } from "./remoteProvider"; | ||
|
||
/** | ||
* Creates a credential provider that will attempt to find credentials from the | ||
* following sources (listed in order of precedence): | ||
* * Environment variables exposed via `process.env` | ||
* * SSO credentials from token cache | ||
* * Web identity token credentials | ||
* * Shared credentials and config ini files | ||
* * The EC2/ECS Instance Metadata Service | ||
* | ||
* The default credential provider will invoke one provider at a time and only | ||
* continue to the next if no credentials have been located. For example, if | ||
* the process finds values defined via the `AWS_ACCESS_KEY_ID` and | ||
* `AWS_SECRET_ACCESS_KEY` environment variables, the files at | ||
* `~/.aws/credentials` and `~/.aws/config` will not be read, nor will any | ||
* messages be sent to the Instance Metadata Service. | ||
* | ||
* @param init Configuration that is passed to each individual | ||
* provider | ||
* | ||
* @see fromEnv The function used to source credentials from | ||
* environment variables | ||
* @see fromSSO The function used to source credentials from | ||
* resolved SSO token cache | ||
* @see fromTokenFile The function used to source credentials from | ||
* token file | ||
* @see fromIni The function used to source credentials from INI | ||
* files | ||
* @see fromProcess The function used to sources credentials from | ||
* credential_process in INI files | ||
* @see fromInstanceMetadata The function used to source credentials from the | ||
* EC2 Instance Metadata Service | ||
* @see fromContainerMetadata The function used to source credentials from the | ||
* ECS Container Metadata Service | ||
*/ | ||
export const defaultProvider = ( | ||
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {} | ||
): CredentialProvider => { | ||
const options = { | ||
profile: process.env[ENV_PROFILE], | ||
...init, | ||
...(!init.loadedConfig && { loadedConfig: loadSharedConfigFiles(init) }), | ||
}; | ||
|
||
const providerChain = chain( | ||
...(options.profile ? [] : [fromEnv()]), | ||
fromSSO(options), | ||
fromIni(options), | ||
fromProcess(options), | ||
fromTokenFile(options), | ||
remoteProvider(options), | ||
async () => { | ||
throw new CredentialsProviderError("Could not load credentials from any providers", false); | ||
} | ||
); | ||
|
||
return memoize( | ||
providerChain, | ||
(credentials) => credentials.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000, | ||
(credentials) => credentials.expiration !== undefined | ||
); | ||
}; |
Oops, something went wrong.