Skip to content

Commit 7841411

Browse files
authored
feat(credential-provider-node): use dynamic import for credential providers (#5677)
* feat(credential-provider-node): use dynamic import for credential providers * test(polly-request-presigner): provide credentials to avoid credential chain * test(aws-client-retry-test): provide credentials to avoid chain in test * test(aws-middleware-test): update jest version * test: mock defaultProvider in integ tests
1 parent 3edef9c commit 7841411

File tree

36 files changed

+827
-436
lines changed

36 files changed

+827
-436
lines changed

Diff for: Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ unlink-smithy:
2020
rm ./node_modules/\@smithy
2121
yarn --check-files
2222

23+
copy-smithy:
24+
node ./scripts/copy-smithy-dist-files
25+
2326
# Runs build for all packages using Turborepo
2427
turbo-build:
2528
(cd scripts/remote-cache && yarn)

Diff for: package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"@tsconfig/recommended": "1.0.1",
7070
"@types/chai-as-promised": "^7.1.2",
7171
"@types/fs-extra": "^8.0.1",
72-
"@types/jest": "29.5.2",
72+
"@types/jest": "29.5.11",
7373
"@typescript-eslint/eslint-plugin": "5.55.0",
7474
"@typescript-eslint/parser": "5.55.0",
7575
"async": "3.2.4",
@@ -93,8 +93,8 @@
9393
"glob": "7.1.6",
9494
"husky": "^4.2.3",
9595
"jasmine-core": "^3.5.0",
96-
"jest": "29.5.0",
97-
"jest-environment-jsdom": "29.5.0",
96+
"jest": "29.7.0",
97+
"jest-environment-jsdom": "29.7.0",
9898
"jmespath": "^0.15.0",
9999
"json5": "^2.2.0",
100100
"karma": "6.4.0",
@@ -113,7 +113,7 @@
113113
"mocha": "10.0.0",
114114
"prettier": "2.8.5",
115115
"rimraf": "3.0.2",
116-
"ts-jest": "29.1.0",
116+
"ts-jest": "29.1.1",
117117
"ts-loader": "9.4.2",
118118
"ts-mocha": "10.0.0",
119119
"ts-node": "10.9.1",

Diff for: packages/credential-provider-node/src/defaultProvider.spec.ts

+53-73
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { fromIni } from "@aws-sdk/credential-provider-ini";
33
import { fromProcess } from "@aws-sdk/credential-provider-process";
44
import { fromSSO } from "@aws-sdk/credential-provider-sso";
55
import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
6-
import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider";
6+
import { CredentialsProviderError } from "@smithy/property-provider";
77
import { ENV_PROFILE, loadSharedConfigFiles } from "@smithy/shared-ini-file-loader";
88

9-
import { defaultProvider } from "./defaultProvider";
9+
import { credentialsTreatedAsExpired, credentialsWillNeedRefresh, defaultProvider } from "./defaultProvider";
1010
import { remoteProvider } from "./remoteProvider";
1111

1212
jest.mock("@aws-sdk/credential-provider-env");
@@ -15,7 +15,6 @@ jest.mock("@aws-sdk/credential-provider-ini");
1515
jest.mock("@aws-sdk/credential-provider-process");
1616
jest.mock("@aws-sdk/credential-provider-sso");
1717
jest.mock("@aws-sdk/credential-provider-web-identity");
18-
jest.mock("@smithy/property-provider");
1918
jest.mock("@smithy/shared-ini-file-loader");
2019
jest.mock("./remoteProvider");
2120

@@ -25,19 +24,24 @@ describe(defaultProvider.name, () => {
2524
secretAccessKey: "mockSecretAccessKey",
2625
};
2726

27+
const credentials = () => {
28+
throw new CredentialsProviderError("test", true);
29+
};
30+
31+
const finalCredentials = () => {
32+
return mockCreds;
33+
};
34+
2835
const mockInit = {
2936
profile: "mockProfile",
3037
};
3138

32-
const mockEnvFn = jest.fn();
33-
const mockSsoFn = jest.fn();
34-
const mockIniFn = jest.fn();
35-
const mockProcessFn = jest.fn();
36-
const mockTokenFileFn = jest.fn();
37-
const mockRemoteProviderFn = jest.fn();
38-
39-
const mockChainFn = jest.fn();
40-
const mockMemoizeFn = jest.fn().mockResolvedValue(mockCreds);
39+
const mockEnvFn = jest.fn().mockImplementation(() => credentials());
40+
const mockSsoFn = jest.fn().mockImplementation(() => credentials());
41+
const mockIniFn = jest.fn().mockImplementation(() => credentials());
42+
const mockProcessFn = jest.fn().mockImplementation(() => credentials());
43+
const mockTokenFileFn = jest.fn().mockImplementation(() => credentials());
44+
const mockRemoteProviderFn = jest.fn().mockImplementation(() => finalCredentials());
4145

4246
beforeEach(() => {
4347
[
@@ -47,51 +51,48 @@ describe(defaultProvider.name, () => {
4751
[fromProcess, mockProcessFn],
4852
[fromTokenFile, mockTokenFileFn],
4953
[remoteProvider, mockRemoteProviderFn],
50-
[chain, mockChainFn],
51-
[memoize, mockMemoizeFn],
5254
].forEach(([fromFn, mockFn]) => {
5355
(fromFn as jest.Mock).mockReturnValue(mockFn);
5456
});
5557
});
5658

5759
afterEach(async () => {
58-
const errorFnIndex = (chain as jest.Mock).mock.calls[0].length;
59-
const errorFn = (chain as jest.Mock).mock.calls[0][errorFnIndex - 1];
60-
const expectedError = new CredentialsProviderError("Could not load credentials from any providers", false);
61-
try {
62-
await errorFn();
63-
fail(`expected ${expectedError}`);
64-
} catch (error) {
65-
expect(error.toString()).toStrictEqual(expectedError.toString());
66-
}
67-
68-
expect(memoize).toHaveBeenCalledWith(mockChainFn, expect.any(Function), expect.any(Function));
69-
7060
jest.clearAllMocks();
7161
});
7262

7363
describe("without fromEnv", () => {
74-
afterEach(() => {
75-
expect(chain).toHaveBeenCalledWith(
76-
mockSsoFn,
77-
mockIniFn,
78-
mockProcessFn,
79-
mockTokenFileFn,
80-
mockRemoteProviderFn,
81-
expect.any(Function)
82-
);
83-
});
84-
8564
it("creates provider chain and memoizes it", async () => {
86-
const receivedCreds = await defaultProvider(mockInit)();
87-
expect(receivedCreds).toStrictEqual(mockCreds);
65+
const provider = defaultProvider(mockInit);
8866

89-
expect(fromEnv).not.toHaveBeenCalled();
90-
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) {
91-
expect(fromFn).toHaveBeenCalledWith(mockInit);
67+
// initial call proceeds through the chain.
68+
{
69+
const receivedCreds = await provider();
70+
expect(receivedCreds).toEqual(mockCreds);
71+
72+
expect(fromEnv).not.toHaveBeenCalled();
73+
expect(fromSSO).toHaveBeenCalledWith(mockInit);
74+
expect(fromIni).toHaveBeenCalledWith(mockInit);
75+
expect(fromProcess).toHaveBeenCalledWith(mockInit);
76+
expect(fromTokenFile).toHaveBeenCalledWith(mockInit);
77+
expect(remoteProvider).toHaveBeenCalledWith(mockInit);
78+
79+
expect(loadSharedConfigFiles).not.toHaveBeenCalled();
9280
}
9381

94-
expect(loadSharedConfigFiles).not.toHaveBeenCalled();
82+
jest.clearAllMocks();
83+
84+
// subsequent call does not enter the chain.
85+
{
86+
const receivedCreds = await provider();
87+
expect(receivedCreds).toEqual(mockCreds);
88+
89+
expect(fromEnv).not.toHaveBeenCalled();
90+
expect(fromSSO).not.toHaveBeenCalledWith(mockInit);
91+
expect(fromIni).not.toHaveBeenCalledWith(mockInit);
92+
expect(fromProcess).not.toHaveBeenCalledWith(mockInit);
93+
expect(fromTokenFile).not.toHaveBeenCalledWith(mockInit);
94+
expect(remoteProvider).not.toHaveBeenCalledWith(mockInit);
95+
}
9596
});
9697

9798
it(`if env['${ENV_PROFILE}'] is set`, async () => {
@@ -123,63 +124,42 @@ describe(defaultProvider.name, () => {
123124
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) {
124125
expect(fromFn).toHaveBeenCalledWith(mockInitWithoutProfile);
125126
}
126-
127-
expect(chain).toHaveBeenCalledWith(
128-
mockEnvFn,
129-
mockSsoFn,
130-
mockIniFn,
131-
mockProcessFn,
132-
mockTokenFileFn,
133-
mockRemoteProviderFn,
134-
expect.any(Function)
135-
);
136127
});
137128

138-
describe("memoize isExpired", () => {
129+
describe(credentialsTreatedAsExpired.name, () => {
139130
const mockDateNow = Date.now();
140131
beforeEach(async () => {
141132
jest.spyOn(Date, "now").mockReturnValueOnce(mockDateNow);
142-
await defaultProvider(mockInit)();
143133
});
144134

145135
it("returns true if expiration is defined, and creds have expired", () => {
146-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
147136
const expiration = new Date(mockDateNow - 24 * 60 * 60 * 1000);
148-
expect(memoizeExpiredFn({ expiration })).toEqual(true);
137+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(true);
149138
});
150139

151140
it("returns true if expiration is defined, and creds expire in <5 mins", () => {
152-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
153141
const expiration = new Date(mockDateNow + 299 * 1000);
154-
expect(memoizeExpiredFn({ expiration })).toEqual(true);
142+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(true);
155143
});
156144

157145
it("returns false if expiration is defined, but creds expire in >5 mins", () => {
158-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
159146
const expiration = new Date(mockDateNow + 301 * 1000);
160-
expect(memoizeExpiredFn({ expiration })).toEqual(false);
147+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(false);
161148
});
162149

163150
it("returns false if expiration is not defined", () => {
164-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
165-
expect(memoizeExpiredFn({})).toEqual(false);
151+
expect(credentialsTreatedAsExpired({ ...mockCreds })).toEqual(false);
166152
});
167153
});
168154

169-
describe("memoize requiresRefresh", () => {
170-
beforeEach(async () => {
171-
await defaultProvider(mockInit)();
172-
});
173-
155+
describe(credentialsWillNeedRefresh.name, () => {
174156
it("returns true if expiration is not defined", () => {
175-
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2];
176-
const expiration = Date.now();
177-
expect(memoizeRefreshFn({ expiration })).toEqual(true);
157+
const expiration = new Date();
158+
expect(credentialsWillNeedRefresh({ ...mockCreds, expiration })).toEqual(true);
178159
});
179160

180161
it("returns false if expiration is not defined", () => {
181-
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2];
182-
expect(memoizeRefreshFn({})).toEqual(false);
162+
expect(credentialsWillNeedRefresh({ ...mockCreds })).toEqual(false);
183163
});
184164
});
185165
});

Diff for: packages/credential-provider-node/src/defaultProvider.ts

+49-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { fromEnv } from "@aws-sdk/credential-provider-env";
2-
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
3-
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
4-
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
5-
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
6-
import { RemoteProviderInit } from "@smithy/credential-provider-imds";
1+
import type { FromIniInit } from "@aws-sdk/credential-provider-ini";
2+
import type { FromProcessInit } from "@aws-sdk/credential-provider-process";
3+
import type { FromSSOInit } from "@aws-sdk/credential-provider-sso";
4+
import type { FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
5+
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
76
import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider";
87
import { ENV_PROFILE } from "@smithy/shared-ini-file-loader";
98
import { AwsCredentialIdentity, MemoizedProvider } from "@smithy/types";
@@ -49,16 +48,52 @@ export type DefaultProviderInit = FromIniInit & RemoteProviderInit & FromProcess
4948
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider<AwsCredentialIdentity> =>
5049
memoize(
5150
chain(
52-
...(init.profile || process.env[ENV_PROFILE] ? [] : [fromEnv()]),
53-
fromSSO(init),
54-
fromIni(init),
55-
fromProcess(init),
56-
fromTokenFile(init),
57-
remoteProvider(init),
51+
...(init.profile || process.env[ENV_PROFILE]
52+
? []
53+
: [
54+
async () => {
55+
const { fromEnv } = await import("@aws-sdk/credential-provider-env");
56+
return fromEnv()();
57+
},
58+
]),
59+
async () => {
60+
const { fromSSO } = await import("@aws-sdk/credential-provider-sso");
61+
return fromSSO(init)();
62+
},
63+
async () => {
64+
const { fromIni } = await import("@aws-sdk/credential-provider-ini");
65+
return fromIni(init)();
66+
},
67+
async () => {
68+
const { fromProcess } = await import("@aws-sdk/credential-provider-process");
69+
return fromProcess(init)();
70+
},
71+
async () => {
72+
const { fromTokenFile } = await import("@aws-sdk/credential-provider-web-identity");
73+
return fromTokenFile(init)();
74+
},
75+
async () => {
76+
return (await remoteProvider(init))();
77+
},
5878
async () => {
5979
throw new CredentialsProviderError("Could not load credentials from any providers", false);
6080
}
6181
),
62-
(credentials) => credentials.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000,
63-
(credentials) => credentials.expiration !== undefined
82+
credentialsTreatedAsExpired,
83+
credentialsWillNeedRefresh
6484
);
85+
86+
/**
87+
* @internal
88+
*
89+
* @returns credentials have expiration.
90+
*/
91+
export const credentialsWillNeedRefresh = (credentials: AwsCredentialIdentity) => credentials?.expiration !== undefined;
92+
93+
/**
94+
* @internal
95+
*
96+
* @returns credentials with less than 5 minutes left.
97+
*/
98+
export const credentialsTreatedAsExpired = (credentials: AwsCredentialIdentity) =>
99+
credentials?.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000;

Diff for: packages/credential-provider-node/src/remoteProvider.spec.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe(remoteProvider.name, () => {
4242
"returns fromContainerMetadata if env['%s'] is set",
4343
async (key) => {
4444
process.env[key] = "defined";
45-
const receivedCreds = await remoteProvider(mockInit)();
45+
const receivedCreds = await (await remoteProvider(mockInit))();
4646
expect(receivedCreds).toStrictEqual(mockCredsFromContainer);
4747
expect(fromContainerMetadata).toHaveBeenCalledWith(mockInit);
4848
expect(fromInstanceMetadata).not.toHaveBeenCalled();
@@ -53,7 +53,9 @@ describe(remoteProvider.name, () => {
5353
process.env[ENV_IMDS_DISABLED] = "1";
5454
const expectedError = new CredentialsProviderError("EC2 Instance Metadata Service access disabled");
5555
try {
56-
await remoteProvider(mockInit)();
56+
await (
57+
await remoteProvider(mockInit)
58+
)();
5759
fail(`expectedError ${expectedError}`);
5860
} catch (error) {
5961
expect(error).toStrictEqual(expectedError);
@@ -63,7 +65,7 @@ describe(remoteProvider.name, () => {
6365
});
6466

6567
it("returns fromInstanceMetadata if environment variables are not set", async () => {
66-
const receivedCreds = await remoteProvider(mockInit)();
68+
const receivedCreds = await (await remoteProvider(mockInit))();
6769
expect(receivedCreds).toStrictEqual(mockSourceCredsFromInstanceMetadata);
6870
expect(fromInstanceMetadata).toHaveBeenCalledWith(mockInit);
6971
expect(fromContainerMetadata).not.toHaveBeenCalled();

Diff for: packages/credential-provider-node/src/remoteProvider.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import {
2-
ENV_CMDS_FULL_URI,
3-
ENV_CMDS_RELATIVE_URI,
4-
fromContainerMetadata,
5-
fromInstanceMetadata,
6-
RemoteProviderInit,
7-
} from "@smithy/credential-provider-imds";
1+
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
82
import { CredentialsProviderError } from "@smithy/property-provider";
9-
import { AwsCredentialIdentityProvider } from "@smithy/types";
3+
import type { AwsCredentialIdentityProvider } from "@smithy/types";
104

115
export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
126

13-
export const remoteProvider = (init: RemoteProviderInit): AwsCredentialIdentityProvider => {
7+
/**
8+
* @internal
9+
*/
10+
export const remoteProvider = async (init: RemoteProviderInit): Promise<AwsCredentialIdentityProvider> => {
11+
const { ENV_CMDS_FULL_URI, ENV_CMDS_RELATIVE_URI, fromContainerMetadata, fromInstanceMetadata } = await import(
12+
"@smithy/credential-provider-imds"
13+
);
14+
1415
if (process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]) {
1516
return fromContainerMetadata(init);
1617
}

Diff for: packages/middleware-endpoint-discovery/src/middleware-endpoint-discovery.integ.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
jest.mock("@aws-sdk/credential-provider-node", () => ({
2+
defaultProvider: async () => {
3+
return {
4+
secretAccessKey: "integration-test",
5+
accessKeyId: "integration-test",
6+
sessionToken: "integration-test",
7+
};
8+
},
9+
}));
110
import { TimestreamQuery } from "@aws-sdk/client-timestream-query";
211
import { TimestreamWrite } from "@aws-sdk/client-timestream-write";
312
import { EndpointCache } from "@aws-sdk/endpoint-cache";

0 commit comments

Comments
 (0)