Skip to content

Commit be8f930

Browse files
committed
Add basic shared file provider
1 parent 245bfdc commit be8f930

File tree

6 files changed

+296
-4
lines changed

6 files changed

+296
-4
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
interface FsModule {
2+
__addMatcher: (toMatch: RegExp, toReturn: string) => void;
3+
__clearMatchers: () => void;
4+
readFile: (path: string, encoding: string, cb: Function) => void
5+
}
6+
7+
const fs: FsModule = <FsModule>jest.genMockFromModule('fs');
8+
const matchers = new Map<RegExp, string>();
9+
10+
function __addMatcher(toMatch: RegExp, toReturn: string): void {
11+
matchers.set(toMatch, toReturn);
12+
}
13+
14+
function __clearMatchers(): void {
15+
matchers.clear();
16+
}
17+
18+
function readFile(
19+
path: string,
20+
encoding: string,
21+
callback: (err: Error|null, data?: string) => void
22+
): void {
23+
for (let [matcher, data] of matchers.entries()) {
24+
if (matcher.test(path)) {
25+
callback(null, data);
26+
return;
27+
}
28+
}
29+
30+
callback(new Error('ENOENT: no such file or directory'));
31+
}
32+
33+
fs.__addMatcher = __addMatcher;
34+
fs.__clearMatchers = __clearMatchers;
35+
fs.readFile = readFile;
36+
37+
module.exports = fs;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface OsModule {
2+
homedir: () => string;
3+
}
4+
5+
const os: OsModule = <OsModule>jest.genMockFromModule('os');
6+
const path = require('path');
7+
8+
os.homedir = () => path.sep + path.join('home', 'user');
9+
10+
module.exports = os;

packages/credential-provider/__tests__/fromEnv.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ describe('fromEnv', () => {
4848
it(
4949
'should reject the promise if no environmental credentials can be found',
5050
async () => {
51-
try {
52-
await fromEnv()();
53-
fail('The promise should have been rejected.');
54-
} catch (e) {}
51+
await fromEnv()().then(
52+
() => { throw new Error('The promise should have been rejected.'); },
53+
() => { /* Promise rejected as expected */ }
54+
);
5555
}
5656
);
5757
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
jest.mock('fs');
2+
jest.mock('os');
3+
4+
const {__addMatcher, __clearMatchers} = require('fs');
5+
const {homedir} = require('os');
6+
7+
import {Buffer} from 'buffer';
8+
import {join} from 'path';
9+
import {fromIni} from "../lib/fromIni";
10+
11+
const DEFAULT_CREDS = {
12+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
13+
secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
14+
sessionToken: 'sessionToken',
15+
};
16+
17+
const FOO_CREDS = {
18+
accessKeyId: 'foo',
19+
secretKey: 'bar',
20+
sessionToken: 'baz',
21+
};
22+
23+
beforeEach(__clearMatchers);
24+
25+
describe('fromIni', () => {
26+
it('should read credentials from ~/.aws/credentials', async () => {
27+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
28+
[default]
29+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
30+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
31+
aws_session_token = ${DEFAULT_CREDS.sessionToken}
32+
`.trim());
33+
34+
expect(await fromIni()()).toEqual(DEFAULT_CREDS);
35+
});
36+
37+
it('should read profile credentials from ~/.aws/credentials', async () => {
38+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
39+
[default]
40+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
41+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
42+
aws_session_token = ${DEFAULT_CREDS.sessionToken}
43+
44+
[foo]
45+
aws_access_key_id = ${FOO_CREDS.accessKeyId}
46+
aws_secret_access_key = ${FOO_CREDS.secretKey}
47+
aws_session_token = ${FOO_CREDS.sessionToken}
48+
`.trim());
49+
50+
expect(await fromIni({profile: 'foo'})()).toEqual(FOO_CREDS);
51+
});
52+
53+
it('should read credentials from ~/.aws/config', async () => {
54+
__addMatcher(new RegExp(join(homedir(), '.aws', 'config')), `
55+
[default]
56+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
57+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
58+
aws_session_token = ${DEFAULT_CREDS.sessionToken}
59+
`.trim());
60+
61+
expect(await fromIni()()).toEqual(DEFAULT_CREDS);
62+
});
63+
64+
it('should read profile credentials from ~/.aws/config', async () => {
65+
__addMatcher(new RegExp(join(homedir(), '.aws', 'config')), `
66+
[default]
67+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
68+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
69+
aws_session_token = ${DEFAULT_CREDS.sessionToken}
70+
71+
[profile foo]
72+
aws_access_key_id = ${FOO_CREDS.accessKeyId}
73+
aws_secret_access_key = ${FOO_CREDS.secretKey}
74+
aws_session_token = ${FOO_CREDS.sessionToken}
75+
`.trim());
76+
77+
expect(await fromIni({profile: 'foo'})()).toEqual(FOO_CREDS);
78+
});
79+
80+
it(
81+
'should prefer credentials in ~/.aws/credentials to those in ~/.aws/config',
82+
async () => {
83+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
84+
[default]
85+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
86+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
87+
aws_session_token = ${DEFAULT_CREDS.sessionToken}
88+
`.trim());
89+
90+
__addMatcher(new RegExp(join(homedir(), '.aws', 'config')), `
91+
[default]
92+
aws_access_key_id = ${FOO_CREDS.accessKeyId}
93+
aws_secret_access_key = ${FOO_CREDS.secretKey}
94+
aws_session_token = ${FOO_CREDS.sessionToken}
95+
`.trim());
96+
97+
expect(await fromIni()()).toEqual(DEFAULT_CREDS);
98+
}
99+
);
100+
101+
it('should reject credentials with no access key key', async () => {
102+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
103+
[default]
104+
aws_secret_access_key = ${DEFAULT_CREDS.secretKey}
105+
`.trim());
106+
107+
await fromIni()().then(
108+
() => { throw new Error('The promise should have been rejected'); },
109+
() => { /* Promise rejected as expected */ }
110+
);
111+
});
112+
113+
it('should reject credentials with no secret key', async () => {
114+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
115+
[default]
116+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
117+
`.trim());
118+
119+
await fromIni()().then(
120+
() => { throw new Error('The promise should have been rejected'); },
121+
() => { /* Promise rejected as expected */ }
122+
);
123+
});
124+
125+
it('should not merge profile values together', async () => {
126+
__addMatcher(new RegExp(join(homedir(), '.aws', 'credentials')), `
127+
[default]
128+
aws_access_key_id = ${DEFAULT_CREDS.accessKeyId}
129+
`.trim());
130+
131+
__addMatcher(new RegExp(join(homedir(), '.aws', 'config')), `
132+
[default]
133+
aws_secret_access_key = ${FOO_CREDS.secretKey}
134+
`.trim());
135+
136+
await fromIni()().then(
137+
() => { throw new Error('The promise should have been rejected'); },
138+
() => { /* Promise rejected as expected */ }
139+
);
140+
});
141+
});

packages/credential-provider/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './lib/chain';
22
export * from './lib/fromCredentials';
33
export * from './lib/fromEnv';
4+
export * from './lib/fromIni';
45
export * from './lib/isCredentials';
56
export * from './lib/memoize';
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {CredentialProvider} from './CredentialProvider';
2+
import {readFile} from 'fs';
3+
import {homedir} from 'os';
4+
import {join} from 'path';
5+
6+
const DEFAULT_PROFILE = 'default';
7+
export const ENV_PROFILE = 'AWS_PROFILE';
8+
const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.aws', 'credentials');
9+
export const ENV_CREDENTIALS_PATH = 'AWS_SHARED_CREDENTIALS_FILE';
10+
const DEFAULT_CONFIG_PATH = join(homedir(), '.aws', 'config');
11+
export const ENV_CONFIG_PATH = 'AWS_CONFIG_FILE';
12+
13+
const INI_KEY = 'aws_access_key_id';
14+
const INI_SECRET = 'aws_secret_access_key';
15+
const INI_SESSION = 'aws_session_token';
16+
17+
export interface fromIniInit {
18+
profile?: string;
19+
filepath?: string;
20+
configFilepath?: string;
21+
}
22+
23+
interface ParsedIniData {
24+
[key: string]: {[key: string]: string};
25+
}
26+
27+
export function fromIni(init: fromIniInit = {}): CredentialProvider {
28+
return () => parseKnownFiles(init).then(profiles => {
29+
const {profile = process.env[ENV_PROFILE] || DEFAULT_PROFILE} = init;
30+
const profileData = profiles[profile];
31+
if (undefined === profileData) {
32+
throw new Error(
33+
`Profile ${profile} not found in shared credentials file.`
34+
);
35+
} else if (!profileData[INI_KEY] || !profileData[INI_SECRET]) {
36+
throw new Error(
37+
`Missing key or secret from profile "${profile}" in shared credentials file.`
38+
);
39+
}
40+
41+
return {
42+
accessKeyId: profileData[INI_KEY],
43+
secretKey: profileData[INI_SECRET],
44+
sessionToken: profileData[INI_SESSION],
45+
};
46+
});
47+
}
48+
49+
function parseIni(iniData: string): ParsedIniData {
50+
const map: ParsedIniData = {};
51+
let currentSection: string|undefined;
52+
for (let line of iniData.split(/\r?\n/)) {
53+
line = line.split(/(^|\s)[;#]/)[0]; // remove comments
54+
const section = line.match(/^\s*\[([^\[\]]+)]\s*$/);
55+
if (section) {
56+
currentSection = section[1];
57+
} else if (currentSection) {
58+
const item = line.match(/^\s*(.+?)\s*=\s*(.+?)\s*$/);
59+
if (item) {
60+
map[currentSection] = map[currentSection] || {};
61+
map[currentSection][item[1]] = item[2];
62+
}
63+
}
64+
}
65+
66+
return map;
67+
}
68+
69+
function parseKnownFiles(init: fromIniInit): Promise<ParsedIniData> {
70+
const {
71+
filepath = process.env[ENV_CREDENTIALS_PATH] || DEFAULT_CREDENTIALS_PATH,
72+
configFilepath = process.env[ENV_CONFIG_PATH] || DEFAULT_CONFIG_PATH,
73+
} = init;
74+
return Promise.all([
75+
slurpFile(configFilepath).then(parseIni).catch(() => { return {}; }),
76+
slurpFile(filepath).then(parseIni).catch(() => { return {}; }),
77+
]).then((parsedFiles: Array<ParsedIniData>) => {
78+
const [config = {}, credentials = {}] = parsedFiles;
79+
const profiles: ParsedIniData = {};
80+
81+
for (let profile of Object.keys(config)) {
82+
profiles[profile.replace(/^profile\s/, '')] = config[profile];
83+
}
84+
85+
for (let profile of Object.keys(credentials)) {
86+
profiles[profile] = credentials[profile];
87+
}
88+
89+
return profiles;
90+
});
91+
}
92+
93+
function slurpFile(path: string): Promise<string> {
94+
return new Promise((resolve, reject) => {
95+
readFile(path, 'utf8', (err, data) => {
96+
if (err) {
97+
reject(err);
98+
} else {
99+
resolve(data);
100+
}
101+
});
102+
});
103+
}

0 commit comments

Comments
 (0)