Skip to content

Commit

Permalink
iss-5 -added the code to build params and tests, updated docs, still …
Browse files Browse the repository at this point in the history
…yet to get file from s3
  • Loading branch information
aceew committed Mar 12, 2017
1 parent 068d564 commit 76b8a08
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 14 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

The purpose of this package is the easily decrypt and fetch environment variables in Lambda functions, using KMS for decryption. The package supports getting environment variables that have been encrypted in Lambda using a default service key, however the main purpose is for decrypting variables that were encrypted using a custom KMS key. For more information on Lambda environment variables and encryption keys, see the [AWS Documentation](http://docs.aws.amazon.com/lambda/latest/dg/env_variables.html).

Before implementing it is recommended you read the [notes](#notes) section
Before implementing it is recommended you read the [FAQs](#faqs) section

## Contents
- [Usage](#usage)
Expand Down Expand Up @@ -116,7 +116,7 @@ Returns the string value of the environment variable. No decryption takes place
## FAQs

### My Lambda config exceeds 4KB. What do I do?
Lambda imposes a 4KB limit on function config, this is inclusive of environment variables. By using a few encrypted environment variables it easy to quickly reach this limit.
Lambda imposes a 4KB limit on function config, this is inclusive of environment variables. and by using a few encrypted environment variables it easy to quickly reach this limit. The way this package recommends using environment variables when the Lambda config reaches 4KB is by storing a file in S3 that is encrypted at rest and pulling this down on the first invocation of the Lambda function and caching it for reduced calls to S3.

### Why is the aws-sdk a dev dependency?
The package depends on the aws-sdk, however it is not listed as a dependency as it should be installed on your lambda environment by default.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"test": "npm run-script lint && nyc ava test && nyc report --reporter=text-lcov | coveralls",
"test-local": "npm run-script lint && nyc ava test && nyc report --reporter=lcov",
"lint": "eslint src/*.js test/*.js",
"build": "babel src -d build && cp package.json build/package.json && cp README.md build/README.md",
"deploy": "npm run-script build && cd build && npm publish"
Expand Down
131 changes: 120 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,44 @@
const AWS = require('aws-sdk');

const decryptedVariables = {};
const s3StoredVariables = {};

export default class LambdaEnvVars {
/**
* Injects the node process API and sets the isntance of AWS KMS.
*
* @param {Object} params
* Default params to be sent with each request.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @param {Object} params.s3Config
* Config used to get the an env var file from S3.
*
* @param {string} params.s3Config.bucketName
* @param {string} params.s3Config.bucketRegion
* @param {string} params.s3Config.fileName
*
* @return {Object}
* Instance of EnvVars.
*/
constructor() {
constructor(params = {}) {
this.defaultParams = {
location: 'lambdaConfig',
s3Config: {},
};

this.defaultParams = Object.assign(this.defaultParams, params);
this.process = process;
this.kms = new AWS.KMS();
this.kms = new AWS.KMS({ apiVersion: '2014-11-01' });
this.s3 = new AWS.S3({ apiVersion: '2006-03-01' });
this.decryptedVariables = decryptedVariables;
this.s3Vars = s3StoredVariables;
this.availableStoreLocations = [
's3',
'lambdaConfig',
];
}

/**
Expand All @@ -36,21 +62,101 @@ export default class LambdaEnvVars {
* @param {string} variableName
* The key in process.env to which the variable is stored under.
*
* @param {Object} params
* Params to state where the environment variable is stored.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @param {Object} params.s3Config
* Config used to get the an env var file from S3.
*
* @param {string} params.s3Config.bucketName
* @param {string} params.s3Config.bucketRegion
* @param {string} params.s3Config.fileName
*
* @return {Promise}
* A promise that resolves the value if it is available, else an empty string if it not set in
* the node environment variables, or a rejected promise if KMS couldn't decypt the value.
*/
getCustomDecryptedValue(variableName = '') {
if (this.decryptedVariables[variableName]) {
return Promise.resolve(this.decryptedVariables[variableName]);
getCustomDecryptedValue(variableName = '', params = {}) {
return this.buildParams(params)
.then((builtParams) => {
if (builtParams.location === 's3') {
return this.getVarFromS3File(variableName, params.s3Config);
}

if (this.decryptedVariables[variableName]) {
return Promise.resolve(this.decryptedVariables[variableName]);
}

if (variableName === '' || !this.process.env[variableName]) {
return Promise.resolve('');
}

return this.decryptVariable(variableName)
.then(result => this.setEncryptedVariable(variableName, result));
});
}

/**
* Gets an env var from a file within S3.
*
* @param {Object}
* @return {Promise}
* Promise that resolves the variable name
*/
getVarFromS3File(variableName = '', s3Config = {}) {
if (
this.s3Vars[s3Config.bucketRegion] &&
this.s3Vars[s3Config.bucketRegion][s3Config.bucketName] &&
this.s3Vars[s3Config.bucketRegion][s3Config.bucketName][s3Config.fileName] &&
this.s3Vars[s3Config.bucketRegion][s3Config.bucketName][s3Config.fileName][variableName]
) {
return Promise.resolve(
this.s3Vars[s3Config.bucketRegion][s3Config.bucketName][s3Config.fileName][variableName],
);
}

if (variableName === '' || !this.process.env[variableName]) {
return Promise.resolve('');
return Promise.resolve();
}

/**
* Validates the parameters that should state where to get the environment variables from. Returns
* the built parameters.
*
* @param {Object} params
* Object containing the parameters.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @return {Promise}
* Resolves the params for the call or rejects an error.
*/
buildParams(params = {}) {
const callParams = Object.assign({}, this.defaultParams, params);

if (this.availableStoreLocations.indexOf(callParams.location) < 0) {
const availableOptions = this.availableStoreLocations.join(', ');
const errorMessage = `Field 'location' must be one of the following ${availableOptions}`;
return Promise.reject(new Error(errorMessage));
}

return this.decryptVariable(variableName)
.then(result => this.setEncryptedVariable(variableName, result));
if (
params.location === 's3' &&
(
!params.s3Config ||
!params.s3Config.bucketName ||
!params.s3Config.bucketRegion ||
!params.s3Config.fileName
)
) {
const errorMessage = 's3Config.bucketName, s3Config.bucketRegion, s3Config.fileName are required when location is \'s3\'';
return Promise.reject(new Error(errorMessage));
}

return Promise.resolve(callParams);
}

/**
Expand All @@ -60,15 +166,18 @@ export default class LambdaEnvVars {
* @param {string[]} variableNames
* An array of environment variable keys to decrypt.
*
* @param {Object} params
* Params to state where the environment variable is stored.
*
* @return {Promise}
* A promise that resolves an object containing the decrypted values where the keys are the items
* specified in the params variableNames.
*/
getCustomDecryptedValueList(variableNames = []) {
getCustomDecryptedValueList(variableNames = [], params = {}) {
const decryptedVariablesObject = {};

const decryptedValuePromiseList = variableNames.map(envVar => (
this.getCustomDecryptedValue(envVar)
this.getCustomDecryptedValue(envVar, params)
.then((decryptedValue) => {
decryptedVariablesObject[envVar] = decryptedValue;
})
Expand Down
110 changes: 109 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ test('Getting simple variables that don\'t exist', (t) => {
test('Getting encrypted vars that have already been decrypted', (t) => {
const variableKey = 'randomVarKey';
lambdaEnvVars.decryptedVariables[variableKey] = 'I have been decrypted';

return lambdaEnvVars.getCustomDecryptedValue(variableKey)
.then((result) => {
t.is(result, lambdaEnvVars.decryptedVariables[variableKey]);
Expand Down Expand Up @@ -99,7 +100,7 @@ test('Decrypting list of env vars returns a promise that resolves the values', (
lambdaEnvVars.process.env[keyName] = 'Some value';
});

return lambdaEnvVars.getCustomDecryptedValueList(keys)
return lambdaEnvVars.getCustomDecryptedValueList(keys, {})
.then((resultObject) => {
t.is(typeof resultObject, 'object');

Expand All @@ -117,3 +118,110 @@ test('Decrypting list of env vars when an empty array is specified, returns empt
t.is(Object.keys(result).length, 0);
})
));

test('Setting default parameters', (t) => {
const defaultEnvVarsInstance = new LambdaEnvVars({ location: 's3' });
t.is(typeof defaultEnvVarsInstance.defaultParams, 'object');
t.is(defaultEnvVarsInstance.defaultParams.location, 's3');
});

test('Building request params: Rejects when location is invalid', (t) => {
const params = { location: 'invalid' };
return lambdaEnvVars.buildParams(params)
.catch((error) => {
t.is(typeof error.message, 'string');
});
});

test('Building request params: Rejects when s3Config is empty', (t) => {
const params = { location: 's3' };
return lambdaEnvVars.buildParams(params)
.catch((error) => {
t.is(typeof error.message, 'string');
});
});

test('Building request params: Rejects when s3Config is missing bucketName', (t) => {
const params = {
location: 's3',
s3Config: {
bucketRegion: 'bucketRegion',
fileName: 'fileName',
},
};

return lambdaEnvVars.buildParams(params)
.catch((error) => {
t.is(typeof error.message, 'string');
});
});

test('Building request params: Rejects when s3Config is missing bucketRegion', (t) => {
const params = {
location: 's3',
s3Config: {
bucketName: 'bucketName',
fileName: 'fileName',
},
};

return lambdaEnvVars.buildParams(params)
.catch((error) => {
t.is(typeof error.message, 'string');
});
});

test('Building request params: Rejects when s3Config is missing fileName', (t) => {
const params = {
location: 's3',
s3Config: {
bucketName: 'bucketName',
bucketRegion: 'bucketRegion',
},
};

return lambdaEnvVars.buildParams(params)
.catch((error) => {
t.is(typeof error.message, 'string');
});
});

test('Building request params: Resolves the params object', (t) => {
const params = {
location: 's3',
s3Config: {
bucketName: 'bucketName',
bucketRegion: 'bucketRegion',
fileName: 'fileName',
},
};

return lambdaEnvVars.buildParams(params)
.then((result) => {
t.is(typeof result, 'object');
});
});

test('Building request params: Uses the default params defined by the constructor', t => (
lambdaEnvVars.buildParams()
.then((result) => {
t.is(typeof result, 'object');
})
));


test('Getting var from s3 file: Resolves the variable when it\'s already set', () => {

});

test('Getting var from s3 file: Rejects when the s3 file is not valid json', () => {

});

test('Getting var from s3 file: Resolves the file once fetched from S3', () => {

});

test('Getting var from s3 file: Resolves the file once fetched from S3', () => {

});

0 comments on commit 76b8a08

Please sign in to comment.