Skip to content

Commit 1b0080d

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 f155187 + f990017 commit 1b0080d

File tree

6 files changed

+330
-0
lines changed

6 files changed

+330
-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: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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(
61+
'should stop after the environmental provider if credentials have been found',
62+
async () => {
63+
const creds = {
64+
accessKeyId: 'foo',
65+
secretAccessKey: 'bar',
66+
};
67+
68+
(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));
69+
70+
expect(await defaultProvider()()).toEqual(creds);
71+
expect((fromEnv() as any).mock.calls.length).toBe(1);
72+
expect((fromIni() as any).mock.calls.length).toBe(0);
73+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
74+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
75+
}
76+
);
77+
78+
it(
79+
'should stop after the ini provider if credentials have been found',
80+
async () => {
81+
const creds = {
82+
accessKeyId: 'foo',
83+
secretAccessKey: 'bar',
84+
};
85+
86+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
87+
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
88+
89+
expect(await defaultProvider()()).toEqual(creds);
90+
expect((fromEnv() as any).mock.calls.length).toBe(1);
91+
expect((fromIni() as any).mock.calls.length).toBe(1);
92+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
93+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
94+
}
95+
);
96+
97+
it(
98+
'should continue on to the IMDS provider if no env or ini credentials have been found',
99+
async () => {
100+
const creds = {
101+
accessKeyId: 'foo',
102+
secretAccessKey: 'bar',
103+
};
104+
105+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
106+
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
107+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
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+
117+
it(
118+
'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',
119+
async () => {
120+
const creds = {
121+
accessKeyId: 'foo',
122+
secretAccessKey: 'bar',
123+
};
124+
125+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
126+
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
127+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error('PANIC')));
128+
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve(creds));
129+
130+
process.env[ENV_CMDS_RELATIVE_URI] = '/credentials';
131+
132+
expect(await defaultProvider()()).toEqual(creds);
133+
expect((fromEnv() as any).mock.calls.length).toBe(1);
134+
expect((fromIni() as any).mock.calls.length).toBe(1);
135+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
136+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
137+
}
138+
);
139+
140+
it('should pass configuration on to the ini provider', async () => {
141+
const iniConfig: FromIniInit = {
142+
profile: 'foo',
143+
mfaCodeProvider: () => Promise.resolve('mfaCode'),
144+
roleAssumer: () => Promise.resolve({
145+
accessKeyId: 'fizz',
146+
secretAccessKey: 'buzz',
147+
}),
148+
filepath: '/home/user/.secrets/credentials.ini',
149+
configFilepath: '/home/user/.secrets/credentials.ini',
150+
};
151+
152+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
153+
(fromIni() as any).mockImplementation(() => Promise.resolve({
154+
accessKeyId: 'foo',
155+
secretAccessKey: 'bar',
156+
}));
157+
158+
(fromIni as any).mockClear();
159+
160+
await expect(defaultProvider(iniConfig)()).resolves;
161+
162+
expect((fromIni as any).mock.calls.length).toBe(1);
163+
expect((fromIni as any).mock.calls[0][0]).toBe(iniConfig);
164+
});
165+
166+
it('should pass configuration on to the IMDS provider', async () => {
167+
const imdsConfig: Ec2InstanceMetadataInit = {
168+
profile: 'foo',
169+
timeout: 2000,
170+
maxRetries: 3,
171+
};
172+
173+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
174+
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
175+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve({
176+
accessKeyId: 'foo',
177+
secretAccessKey: 'bar',
178+
}));
179+
180+
(fromInstanceMetadata as any).mockClear();
181+
182+
await expect(defaultProvider(imdsConfig)()).resolves;
183+
184+
expect((fromInstanceMetadata as any).mock.calls.length).toBe(1);
185+
expect((fromInstanceMetadata as any).mock.calls[0][0]).toBe(imdsConfig);
186+
});
187+
188+
it('should pass configuration on to the ECS IMDS provider', async () => {
189+
const ecsImdsConfig: RemoteProviderInit = {
190+
timeout: 2000,
191+
maxRetries: 3,
192+
};
193+
194+
(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
195+
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
196+
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve({
197+
accessKeyId: 'foo',
198+
secretAccessKey: 'bar',
199+
}));
200+
201+
(fromContainerMetadata as any).mockClear();
202+
203+
process.env[ENV_CMDS_RELATIVE_URI] = '/credentials';
204+
205+
await expect(defaultProvider(ecsImdsConfig)()).resolves;
206+
207+
expect((fromContainerMetadata as any).mock.calls.length).toBe(1);
208+
expect((fromContainerMetadata as any).mock.calls[0][0]).toBe(ecsImdsConfig);
209+
});
210+
211+
it('should return the same promise across invocations', async () => {
212+
const creds = {
213+
accessKeyId: 'foo',
214+
secretAccessKey: 'bar',
215+
};
216+
217+
(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));
218+
219+
const provider = defaultProvider();
220+
221+
expect(await provider()).toEqual(creds);
222+
223+
expect(provider()).toBe(provider());
224+
225+
expect(await provider()).toEqual(creds);
226+
expect((fromEnv() as any).mock.calls.length).toBe(1);
227+
});
228+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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(chain(
44+
fromEnv(),
45+
fromIni(init),
46+
process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]
47+
? fromContainerMetadata(init)
48+
: fromInstanceMetadata(init)
49+
));
50+
}
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)