Skip to content

Add a default credential provider package for use in Node applications #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/credential-provider-imds/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/fromContainerMetadata';
export * from './lib/fromInstanceMetadata';
export * from './lib/remoteProvider/RemoteProviderInit';
4 changes: 4 additions & 0 deletions packages/default-credential-provider/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/
*.js
*.js.map
*.d.ts
228 changes: 228 additions & 0 deletions packages/default-credential-provider/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import {defaultProvider} from "../";
import {CredentialError} from "@aws/credential-provider-base";

jest.mock('@aws/credential-provider-env', () => {
const envProvider = jest.fn();
return {
fromEnv: jest.fn(() => envProvider),
};
});
import {fromEnv} from '@aws/credential-provider-env';

jest.mock('@aws/credential-provider-ini', () => {
const iniProvider = jest.fn();
return {
fromIni: jest.fn(() => iniProvider),
};
});
import {fromIni, FromIniInit} from '@aws/credential-provider-ini';

jest.mock('@aws/credential-provider-imds', () => {
const containerMdsProvider = jest.fn();
const instanceMdsProvider = jest.fn();
return {
fromContainerMetadata: jest.fn(() => containerMdsProvider),
fromInstanceMetadata: jest.fn(() => instanceMdsProvider),
};
});
import {
Ec2InstanceMetadataInit,
ENV_CMDS_FULL_URI,
ENV_CMDS_RELATIVE_URI,
fromContainerMetadata,
fromInstanceMetadata,
RemoteProviderInit,
} from '@aws/credential-provider-imds';

const fullUri = process.env[ENV_CMDS_FULL_URI];
const relativeUri = process.env[ENV_CMDS_RELATIVE_URI];

beforeEach(() => {
delete process.env[ENV_CMDS_FULL_URI];
delete process.env[ENV_CMDS_RELATIVE_URI];

(fromEnv() as any).mockClear();
(fromIni() as any).mockClear();
(fromContainerMetadata() as any).mockClear();
(fromInstanceMetadata() as any).mockClear();
(fromEnv as any).mockClear();
(fromIni as any).mockClear();
(fromContainerMetadata as any).mockClear();
(fromInstanceMetadata as any).mockClear();
});

afterAll(() => {
process.env[ENV_CMDS_FULL_URI] = fullUri;
process.env[ENV_CMDS_RELATIVE_URI] = relativeUri;
});

describe('defaultProvider', () => {
it(
'should stop after the environmental provider if credentials have been found',
async () => {
const creds = {
accessKeyId: 'foo',
secretAccessKey: 'bar',
};

(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
}
);

it(
'should stop after the ini provider if credentials have been found',
async () => {
const creds = {
accessKeyId: 'foo',
secretAccessKey: 'bar',
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
}
);

it(
'should continue on to the IMDS provider if no env or ini credentials have been found',
async () => {
const creds = {
accessKeyId: 'foo',
secretAccessKey: 'bar',
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(1);
}
);

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 () => {
const creds = {
accessKeyId: 'foo',
secretAccessKey: 'bar',
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error('PANIC')));
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve(creds));

process.env[ENV_CMDS_RELATIVE_URI] = '/credentials';

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
}
);

it('should pass configuration on to the ini provider', async () => {
const iniConfig: FromIniInit = {
profile: 'foo',
mfaCodeProvider: () => Promise.resolve('mfaCode'),
roleAssumer: () => Promise.resolve({
accessKeyId: 'fizz',
secretAccessKey: 'buzz',
}),
filepath: '/home/user/.secrets/credentials.ini',
configFilepath: '/home/user/.secrets/credentials.ini',
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
(fromIni() as any).mockImplementation(() => Promise.resolve({
accessKeyId: 'foo',
secretAccessKey: 'bar',
}));

(fromIni as any).mockClear();

await expect(defaultProvider(iniConfig)()).resolves;

expect((fromIni as any).mock.calls.length).toBe(1);
expect((fromIni as any).mock.calls[0][0]).toBe(iniConfig);
});

it('should pass configuration on to the IMDS provider', async () => {
const imdsConfig: Ec2InstanceMetadataInit = {
profile: 'foo',
timeout: 2000,
maxRetries: 3,
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve({
accessKeyId: 'foo',
secretAccessKey: 'bar',
}));

(fromInstanceMetadata as any).mockClear();

await expect(defaultProvider(imdsConfig)()).resolves;

expect((fromInstanceMetadata as any).mock.calls.length).toBe(1);
expect((fromInstanceMetadata as any).mock.calls[0][0]).toBe(imdsConfig);
});

it('should pass configuration on to the ECS IMDS provider', async () => {
const ecsImdsConfig: RemoteProviderInit = {
timeout: 2000,
maxRetries: 3,
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new CredentialError('Keep moving!')));
(fromIni() as any).mockImplementation(() => Promise.reject(new CredentialError('Nothing here!')));
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve({
accessKeyId: 'foo',
secretAccessKey: 'bar',
}));

(fromContainerMetadata as any).mockClear();

process.env[ENV_CMDS_RELATIVE_URI] = '/credentials';

await expect(defaultProvider(ecsImdsConfig)()).resolves;

expect((fromContainerMetadata as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata as any).mock.calls[0][0]).toBe(ecsImdsConfig);
});

it('should return the same promise across invocations', async () => {
const creds = {
accessKeyId: 'foo',
secretAccessKey: 'bar',
};

(fromEnv() as any).mockImplementation(() => Promise.resolve(creds));

const provider = defaultProvider();

expect(await provider()).toEqual(creds);

expect(provider()).toBe(provider());

expect(await provider()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
});
});
50 changes: 50 additions & 0 deletions packages/default-credential-provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {chain, memoize} from '@aws/credential-provider-base';
import {fromEnv} from '@aws/credential-provider-env';
import {
Ec2InstanceMetadataInit,
ENV_CMDS_FULL_URI,
ENV_CMDS_RELATIVE_URI,
fromContainerMetadata,
fromInstanceMetadata,
RemoteProviderInit,
} from '@aws/credential-provider-imds';
import {fromIni, FromIniInit} from '@aws/credential-provider-ini';
import {CredentialProvider} from '@aws/types';

/**
* 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`
* * 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 fromIni The function used to source credentials from 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 function defaultProvider(
init: Ec2InstanceMetadataInit & FromIniInit & RemoteProviderInit = {}
): CredentialProvider {
return memoize(chain(
fromEnv(),
fromIni(init),
process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]
? fromContainerMetadata(init)
: fromInstanceMetadata(init)
));
}
34 changes: 34 additions & 0 deletions packages/default-credential-provider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@aws/default-credential-provider",
"version": "0.0.1",
"private": true,
"description": "AWS credential provider that sources credentials from a Node.JS environment",
"engines": {
"node": ">=4.0"
},
"main": "index.js",
"scripts": {
"prepublishOnly": "tsc",
"pretest": "tsc",
"test": "jest"
},
"keywords": [
"aws",
"credentials"
],
"author": "[email protected]",
"license": "UNLICENSED",
"dependencies": {
"@aws/credential-provider-base": "^0.0.1",
"@aws/credential-provider-env": "^0.0.1",
"@aws/credential-provider-imds": "^0.0.1",
"@aws/credential-provider-ini": "^0.0.1",
"@aws/types": "^0.0.1"
},
"devDependencies": {
"@types/jest": "^20.0.2",
"@types/node": "^7.0.12",
"jest": "^20.0.4",
"typescript": "^2.3"
}
}
13 changes: 13 additions & 0 deletions packages/default-credential-provider/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"declaration": true,
"strict": true,
"sourceMap": true,
"lib": [
"es5",
"es2015.promise"
]
}
}