Skip to content

Commit c2cde36

Browse files
authored
Merge pull request #30 from jeskew/feature/default-provider-chain
Add a default credential provider package for use in Node applications
2 parents ed2498d + dec3ac8 commit c2cde36

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./lib/fromContainerMetadata";
22
export * from "./lib/fromInstanceMetadata";
3+
export * from "./lib/remoteProvider/RemoteProviderInit";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { defaultProvider } from "../";
2+
import { CredentialError } from "@aws/credential-provider-base";
3+
4+
jest.mock("@aws/credential-provider-env", () => {
5+
const envProvider = jest.fn();
6+
return {
7+
fromEnv: jest.fn(() => envProvider)
8+
};
9+
});
10+
import { fromEnv } from "@aws/credential-provider-env";
11+
12+
jest.mock("@aws/credential-provider-ini", () => {
13+
const iniProvider = jest.fn();
14+
return {
15+
fromIni: jest.fn(() => iniProvider)
16+
};
17+
});
18+
import { fromIni, FromIniInit } from "@aws/credential-provider-ini";
19+
20+
jest.mock("@aws/credential-provider-imds", () => {
21+
const containerMdsProvider = jest.fn();
22+
const instanceMdsProvider = jest.fn();
23+
return {
24+
fromContainerMetadata: jest.fn(() => containerMdsProvider),
25+
fromInstanceMetadata: jest.fn(() => instanceMdsProvider)
26+
};
27+
});
28+
import {
29+
Ec2InstanceMetadataInit,
30+
ENV_CMDS_FULL_URI,
31+
ENV_CMDS_RELATIVE_URI,
32+
fromContainerMetadata,
33+
fromInstanceMetadata,
34+
RemoteProviderInit
35+
} from "@aws/credential-provider-imds";
36+
37+
const fullUri = process.env[ENV_CMDS_FULL_URI];
38+
const relativeUri = process.env[ENV_CMDS_RELATIVE_URI];
39+
40+
beforeEach(() => {
41+
delete process.env[ENV_CMDS_FULL_URI];
42+
delete process.env[ENV_CMDS_RELATIVE_URI];
43+
44+
(fromEnv() as any).mockClear();
45+
(fromIni() as any).mockClear();
46+
(fromContainerMetadata() as any).mockClear();
47+
(fromInstanceMetadata() as any).mockClear();
48+
(fromEnv as any).mockClear();
49+
(fromIni as any).mockClear();
50+
(fromContainerMetadata as any).mockClear();
51+
(fromInstanceMetadata as any).mockClear();
52+
});
53+
54+
afterAll(() => {
55+
process.env[ENV_CMDS_FULL_URI] = fullUri;
56+
process.env[ENV_CMDS_RELATIVE_URI] = relativeUri;
57+
});
58+
59+
describe("defaultProvider", () => {
60+
it("should stop after the environmental provider if credentials have been found", async () => {
61+
const creds = {
62+
accessKeyId: "foo",
63+
secretAccessKey: "bar"
64+
};
65+
66+
(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));
67+
68+
expect(await defaultProvider()()).toEqual(creds);
69+
expect((fromEnv() as any).mock.calls.length).toBe(1);
70+
expect((fromIni() as any).mock.calls.length).toBe(0);
71+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
72+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
73+
});
74+
75+
it("should stop after the ini provider if credentials have been found", async () => {
76+
const creds = {
77+
accessKeyId: "foo",
78+
secretAccessKey: "bar"
79+
};
80+
81+
(fromEnv() as any).mockImplementation(() =>
82+
Promise.reject(new CredentialError("Nothing here!"))
83+
);
84+
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
85+
86+
expect(await defaultProvider()()).toEqual(creds);
87+
expect((fromEnv() as any).mock.calls.length).toBe(1);
88+
expect((fromIni() as any).mock.calls.length).toBe(1);
89+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
90+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
91+
});
92+
93+
it("should continue on to the IMDS provider if no env or ini credentials have been found", async () => {
94+
const creds = {
95+
accessKeyId: "foo",
96+
secretAccessKey: "bar"
97+
};
98+
99+
(fromEnv() as any).mockImplementation(() =>
100+
Promise.reject(new CredentialError("Keep moving!"))
101+
);
102+
(fromIni() as any).mockImplementation(() =>
103+
Promise.reject(new CredentialError("Nothing here!"))
104+
);
105+
(fromInstanceMetadata() as any).mockImplementation(() =>
106+
Promise.resolve(creds)
107+
);
108+
109+
expect(await defaultProvider()()).toEqual(creds);
110+
expect((fromEnv() as any).mock.calls.length).toBe(1);
111+
expect((fromIni() as any).mock.calls.length).toBe(1);
112+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
113+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(1);
114+
});
115+
116+
it("should continue on to the ECS IMDS provider if no env or ini credentials have been found and an ECS environment variable has been set", async () => {
117+
const creds = {
118+
accessKeyId: "foo",
119+
secretAccessKey: "bar"
120+
};
121+
122+
(fromEnv() as any).mockImplementation(() =>
123+
Promise.reject(new CredentialError("Keep moving!"))
124+
);
125+
(fromIni() as any).mockImplementation(() =>
126+
Promise.reject(new CredentialError("Nothing here!"))
127+
);
128+
(fromInstanceMetadata() as any).mockImplementation(() =>
129+
Promise.reject(new Error("PANIC"))
130+
);
131+
(fromContainerMetadata() as any).mockImplementation(() =>
132+
Promise.resolve(creds)
133+
);
134+
135+
process.env[ENV_CMDS_RELATIVE_URI] = "/credentials";
136+
137+
expect(await defaultProvider()()).toEqual(creds);
138+
expect((fromEnv() as any).mock.calls.length).toBe(1);
139+
expect((fromIni() as any).mock.calls.length).toBe(1);
140+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
141+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
142+
});
143+
144+
it("should pass configuration on to the ini provider", async () => {
145+
const iniConfig: FromIniInit = {
146+
profile: "foo",
147+
mfaCodeProvider: () => Promise.resolve("mfaCode"),
148+
roleAssumer: () =>
149+
Promise.resolve({
150+
accessKeyId: "fizz",
151+
secretAccessKey: "buzz"
152+
}),
153+
filepath: "/home/user/.secrets/credentials.ini",
154+
configFilepath: "/home/user/.secrets/credentials.ini"
155+
};
156+
157+
(fromEnv() as any).mockImplementation(() =>
158+
Promise.reject(new CredentialError("Keep moving!"))
159+
);
160+
(fromIni() as any).mockImplementation(() =>
161+
Promise.resolve({
162+
accessKeyId: "foo",
163+
secretAccessKey: "bar"
164+
})
165+
);
166+
167+
(fromIni as any).mockClear();
168+
169+
await expect(defaultProvider(iniConfig)()).resolves;
170+
171+
expect((fromIni as any).mock.calls.length).toBe(1);
172+
expect((fromIni as any).mock.calls[0][0]).toBe(iniConfig);
173+
});
174+
175+
it("should pass configuration on to the IMDS provider", async () => {
176+
const imdsConfig: Ec2InstanceMetadataInit = {
177+
profile: "foo",
178+
timeout: 2000,
179+
maxRetries: 3
180+
};
181+
182+
(fromEnv() as any).mockImplementation(() =>
183+
Promise.reject(new CredentialError("Keep moving!"))
184+
);
185+
(fromIni() as any).mockImplementation(() =>
186+
Promise.reject(new CredentialError("Nothing here!"))
187+
);
188+
(fromInstanceMetadata() as any).mockImplementation(() =>
189+
Promise.resolve({
190+
accessKeyId: "foo",
191+
secretAccessKey: "bar"
192+
})
193+
);
194+
195+
(fromInstanceMetadata as any).mockClear();
196+
197+
await expect(defaultProvider(imdsConfig)()).resolves;
198+
199+
expect((fromInstanceMetadata as any).mock.calls.length).toBe(1);
200+
expect((fromInstanceMetadata as any).mock.calls[0][0]).toBe(imdsConfig);
201+
});
202+
203+
it("should pass configuration on to the ECS IMDS provider", async () => {
204+
const ecsImdsConfig: RemoteProviderInit = {
205+
timeout: 2000,
206+
maxRetries: 3
207+
};
208+
209+
(fromEnv() as any).mockImplementation(() =>
210+
Promise.reject(new CredentialError("Keep moving!"))
211+
);
212+
(fromIni() as any).mockImplementation(() =>
213+
Promise.reject(new CredentialError("Nothing here!"))
214+
);
215+
(fromContainerMetadata() as any).mockImplementation(() =>
216+
Promise.resolve({
217+
accessKeyId: "foo",
218+
secretAccessKey: "bar"
219+
})
220+
);
221+
222+
(fromContainerMetadata as any).mockClear();
223+
224+
process.env[ENV_CMDS_RELATIVE_URI] = "/credentials";
225+
226+
await expect(defaultProvider(ecsImdsConfig)()).resolves;
227+
228+
expect((fromContainerMetadata as any).mock.calls.length).toBe(1);
229+
expect((fromContainerMetadata as any).mock.calls[0][0]).toBe(ecsImdsConfig);
230+
});
231+
232+
it("should return the same promise across invocations", async () => {
233+
const creds = {
234+
accessKeyId: "foo",
235+
secretAccessKey: "bar"
236+
};
237+
238+
(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));
239+
240+
const provider = defaultProvider();
241+
242+
expect(await provider()).toEqual(creds);
243+
244+
expect(provider()).toBe(provider());
245+
246+
expect(await provider()).toEqual(creds);
247+
expect((fromEnv() as any).mock.calls.length).toBe(1);
248+
});
249+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { chain, memoize } from "@aws/credential-provider-base";
2+
import { fromEnv } from "@aws/credential-provider-env";
3+
import {
4+
Ec2InstanceMetadataInit,
5+
ENV_CMDS_FULL_URI,
6+
ENV_CMDS_RELATIVE_URI,
7+
fromContainerMetadata,
8+
fromInstanceMetadata,
9+
RemoteProviderInit
10+
} from "@aws/credential-provider-imds";
11+
import { fromIni, FromIniInit } from "@aws/credential-provider-ini";
12+
import { CredentialProvider } from "@aws/types";
13+
14+
/**
15+
* Creates a credential provider that will attempt to find credentials from the
16+
* following sources (listed in order of precedence):
17+
* * Environment variables exposed via `process.env`
18+
* * Shared credentials and config ini files
19+
* * The EC2/ECS Instance Metadata Service
20+
*
21+
* The default credential provider will invoke one provider at a time and only
22+
* continue to the next if no credentials have been located. For example, if
23+
* the process finds values defined via the `AWS_ACCESS_KEY_ID` and
24+
* `AWS_SECRET_ACCESS_KEY` environment variables, the files at
25+
* `~/.aws/credentials` and `~/.aws/config` will not be read, nor will any
26+
* messages be sent to the Instance Metadata Service.
27+
*
28+
* @param init Configuration that is passed to each individual
29+
* provider
30+
*
31+
* @see fromEnv The function used to source credentials from
32+
* environment variables
33+
* @see fromIni The function used to source credentials from INI
34+
* files
35+
* @see fromInstanceMetadata The function used to source credentials from the
36+
* EC2 Instance Metadata Service
37+
* @see fromContainerMetadata The function used to source credentials from the
38+
* ECS Container Metadata Service
39+
*/
40+
export function defaultProvider(
41+
init: Ec2InstanceMetadataInit & FromIniInit & RemoteProviderInit = {}
42+
): CredentialProvider {
43+
return memoize(
44+
chain(
45+
fromEnv(),
46+
fromIni(init),
47+
process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]
48+
? fromContainerMetadata(init)
49+
: fromInstanceMetadata(init)
50+
)
51+
);
52+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@aws/default-credential-provider",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "AWS credential provider that sources credentials from a Node.JS environment",
6+
"engines": {
7+
"node": ">=4.0"
8+
},
9+
"main": "index.js",
10+
"scripts": {
11+
"prepublishOnly": "tsc",
12+
"pretest": "tsc",
13+
"test": "jest"
14+
},
15+
"keywords": [
16+
"aws",
17+
"credentials"
18+
],
19+
"author": "[email protected]",
20+
"license": "UNLICENSED",
21+
"dependencies": {
22+
"@aws/credential-provider-base": "^0.0.1",
23+
"@aws/credential-provider-env": "^0.0.1",
24+
"@aws/credential-provider-imds": "^0.0.1",
25+
"@aws/credential-provider-ini": "^0.0.1",
26+
"@aws/types": "^0.0.1"
27+
},
28+
"devDependencies": {
29+
"@types/jest": "^20.0.2",
30+
"@types/node": "^7.0.12",
31+
"jest": "^20.0.4",
32+
"typescript": "^2.3"
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es5",
5+
"declaration": true,
6+
"strict": true,
7+
"sourceMap": true,
8+
"lib": [
9+
"es5",
10+
"es2015.promise"
11+
]
12+
}
13+
}

0 commit comments

Comments
 (0)