Comparing version
@@ -50,5 +50,7 @@ import { SSM } from 'aws-sdk' | ||
cache?: boolean; | ||
params: { [key: string]: string; }; | ||
paths?: { [key: string]: string; }; | ||
names?: { [key: string]: string; }; | ||
awsSdkOptions?: Partial<SSM.Types.ClientConfiguration>; | ||
setToContext?: boolean; | ||
getParamNameFromPath?: (path: string, name: string, prefix: string) => string; | ||
} | ||
@@ -55,0 +57,0 @@ |
{ | ||
"name": "middy", | ||
"version": "0.11.2", | ||
"version": "0.12.0", | ||
"description": "🛵 The stylish Node.js middleware engine for AWS Lambda", | ||
@@ -5,0 +5,0 @@ "main": "./index.js", |
@@ -505,3 +505,3 @@ <div align="center"> | ||
- [`httpPartialResponse`](/docs/middlewares.md#httppartialresponse): Filter response objects attributes based on query string parameters. | ||
- [`jsonBodyParser`](/docs/middlewares.md#jsonbodyparser): automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of | ||
- [`jsonBodyParser`](/docs/middlewares.md#jsonbodyparser): Automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of | ||
`httpErrorHanler`. | ||
@@ -597,5 +597,3 @@ - [`s3KeyNormalizer`](/docs/middlewares.md#s3keynormalizer): Normalizes key names in s3 events. | ||
**Kind**: global typedef | ||
**Returns**: <code>void</code> \| <code>Promise</code> - - A middleware can return a Promise instead of using the `next` function as a callback. | ||
In this case middy will wait for the promise to resolve (or reject) and it will automatically | ||
propagate the result to the next middleware. | ||
**Returns**: <code>void</code> \| <code>Promise</code> - - A middleware can return a Promise instead of using the `next` function as a callback. In this case middy will wait for the promise to resolve (or reject) and it will automatically propagate the result to the next middleware. | ||
@@ -602,0 +600,0 @@ | Param | Type | Description | |
@@ -10,2 +10,4 @@ jest.mock('aws-sdk') | ||
SSM.prototype.getParameters = getParametersMock | ||
const getParametersByPathMock = jest.fn() | ||
SSM.prototype.getParametersByPath = getParametersByPathMock | ||
@@ -15,3 +17,7 @@ beforeEach(() => { | ||
getParametersMock.mockClear() | ||
getParametersByPathMock.mockReset() | ||
getParametersByPathMock.mockClear() | ||
delete process.env.MONGO_URL | ||
delete process.env.OTHER_MONGO_URL | ||
delete process.env.SERVICE_NAME_MONGO_URL | ||
}) | ||
@@ -24,2 +30,6 @@ | ||
getParametersByPathMock.mockReturnValue({ | ||
promise: () => Promise.resolve(ssmMockResponse) | ||
}) | ||
const handler = middy((event, context, cb) => { | ||
@@ -42,3 +52,3 @@ cb() | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
MONGO_URL: '/dev/service_name/mongo_url' | ||
@@ -63,3 +73,3 @@ } | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
MONGO_URL: '/dev/service_name/mongo_url' | ||
@@ -86,3 +96,3 @@ }, | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
secureValue: '/dev/service_name/secure_param' | ||
@@ -106,7 +116,8 @@ }, | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
secureValue: '/dev/service_name/secure_param' | ||
}, | ||
cache: true, | ||
setToContext: true | ||
setToContext: true, | ||
paramsLoaded: false | ||
}, | ||
@@ -126,3 +137,3 @@ cb () { | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
secureValue: '/dev/service_name/secure_param' | ||
@@ -145,3 +156,3 @@ }, | ||
middlewareOptions: { | ||
params: { | ||
names: { | ||
invalidParam: 'invalid-smm-param-name', | ||
@@ -168,2 +179,33 @@ anotherInvalidParam: 'another-invalid-ssm-param' | ||
}) | ||
test('It should set properties on target with names equal to full parameter name sans specified path', (done) => { | ||
testScenario({ | ||
ssmMockResponse: { | ||
Parameters: [{Name: '/dev/service_name/mongo_url', Value: 'my-mongo-url'}] | ||
}, | ||
middlewareOptions: { | ||
paths: {'': '/dev/service_name'} | ||
}, | ||
cb () { | ||
expect(process.env.MONGO_URL).toEqual('my-mongo-url') | ||
done() | ||
} | ||
}) | ||
}) | ||
test('It should retrieve params from multiple paths', (done) => { | ||
testScenario({ | ||
ssmMockResponse: { | ||
Parameters: [{Name: '/dev/service_name/mongo_url', Value: 'my-mongo-url'}] | ||
}, | ||
middlewareOptions: { | ||
paths: {'': ['/dev/service_name'], 'prefix': '/dev'} | ||
}, | ||
cb () { | ||
expect(process.env.MONGO_URL).toEqual('my-mongo-url') | ||
expect(process.env.PREFIX_SERVICE_NAME_MONGO_URL).toEqual('my-mongo-url') | ||
done() | ||
} | ||
}) | ||
}) | ||
}) |
@@ -0,12 +1,16 @@ | ||
let paramsLoaded = false | ||
let ssmInstance | ||
module.exports = (opts) => { | ||
module.exports = opts => { | ||
const defaults = { | ||
awsSdkOptions: { | ||
maxRetries: 6, // lowers a chance to hit service rate limits, default is 3 | ||
retryDelayOptions: {base: 200} | ||
retryDelayOptions: { base: 200 } | ||
}, | ||
params: {}, | ||
paths: {}, | ||
names: {}, | ||
getParamNameFromPath: getParamNameFromPathDefault, | ||
setToContext: false, | ||
cache: false | ||
cache: false, | ||
paramsLoaded: paramsLoaded | ||
} | ||
@@ -16,50 +20,66 @@ | ||
return ({ | ||
before (handler, next) { | ||
const targetParamsObject = getTargetObjectToAssign(handler, options) | ||
const stillCached = areParamsStillCached(options, targetParamsObject) | ||
return { | ||
before: (handler, next) => { | ||
if (options.cache && options.paramsLoaded) return next() | ||
if (stillCached) { | ||
return next() | ||
ssmInstance = ssmInstance || getSSMInstance(options.awsSdkOptions) | ||
const ssmPromises = Object.keys(options.paths).reduce((aggregator, prefix) => { | ||
const pathsData = options.paths[prefix] | ||
const paths = Array.isArray(pathsData) ? pathsData : [pathsData] | ||
return paths.reduce((subAggregator, path) => { | ||
subAggregator.push( | ||
ssmInstance | ||
.getParametersByPath({ Path: path, Recursive: true, WithDecryption: true }) | ||
.promise() | ||
.then(handleInvalidParams) | ||
.then(ssmResponse => getParamsToAssignByPath(path, ssmResponse, prefix, options.getParamNameFromPath)) | ||
) | ||
return subAggregator | ||
}, aggregator) | ||
}, []) | ||
const ssmParamNames = getSSMParamValues(options.names) | ||
if (ssmParamNames.length) { | ||
ssmPromises.push( | ||
ssmInstance | ||
.getParameters({ Names: ssmParamNames, WithDecryption: true }) | ||
.promise() | ||
.then(handleInvalidParams) | ||
.then(ssmResponse => getParamsToAssignByName(options.names, ssmResponse)) | ||
) | ||
} | ||
const ssmParamNames = getSSMParamNames(options.params) | ||
lazilyLoadSSMInstance(options.awsSdkOptions) | ||
return getSSMParams(ssmParamNames) | ||
.then(ssmResponse => { | ||
assignSSMParamsToTarget(targetParamsObject, options.params, ssmResponse) | ||
return Promise.all(ssmPromises).then(objectsToMap => { | ||
const targetParamsObject = getTargetObjectToAssign(handler, options) | ||
objectsToMap.forEach(object => { | ||
Object.assign(targetParamsObject, object) | ||
}) | ||
paramsLoaded = true | ||
}) | ||
} | ||
}) | ||
} | ||
function getTargetObjectToAssign (handler, options) { | ||
if (options.setToContext) { | ||
return handler.context | ||
} | ||
return process.env | ||
} | ||
function areParamsStillCached (options, targetParamsObject) { | ||
if (!options.cache) { | ||
return false | ||
} | ||
// returns full parameter name sans the path as specified, with slashes replaced with underscores and any prefix applied | ||
// everything gets upper cased | ||
// e.g. if path is '/dev/myApi/', the parameter '/dev/myApi/connString/default' will be returned with the name 'CONNSTRING_DEFAULT' | ||
// see: https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-su-organize.html | ||
const getParamNameFromPathDefault = (path, name, prefix) => { | ||
const localName = name | ||
.split(`${path}/`) | ||
.join(``) // replace path | ||
.split(`/`) | ||
.join(`_`) // replace remaining slashes with underscores | ||
for (const userParamsName in options.params) { | ||
if (options.params.hasOwnProperty(userParamsName)) { | ||
if (typeof targetParamsObject[userParamsName] === 'undefined') { | ||
return false | ||
} | ||
} | ||
} | ||
const fullLocalName = prefix ? `${prefix}_${localName}` : localName | ||
return true | ||
return fullLocalName.toUpperCase() | ||
} | ||
function getSSMParamNames (userParamsMap) { | ||
return Object.keys(userParamsMap).map(key => userParamsMap[key]) | ||
} | ||
const getTargetObjectToAssign = (handler, options) => (options.setToContext ? handler.context : process.env) | ||
const getSSMParamValues = userParamsMap => Object.keys(userParamsMap).map(key => userParamsMap[key]) | ||
/** | ||
@@ -72,49 +92,39 @@ * Lazily load aws-sdk and initialize SSM constructor | ||
*/ | ||
function lazilyLoadSSMInstance (awsSdkOptions) { | ||
const getSSMInstance = awsSdkOptions => { | ||
// lazy load aws-sdk and SSM constructor to avoid performance | ||
// penalties if you don't use this middleware | ||
if (ssmInstance) { | ||
return ssmInstance | ||
} | ||
// AWS Lambda has aws-sdk included version 2.176.0 | ||
// see https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html | ||
const {SSM} = require('aws-sdk') | ||
ssmInstance = new SSM(awsSdkOptions) | ||
const { SSM } = require('aws-sdk') | ||
return new SSM(awsSdkOptions) | ||
} | ||
/** | ||
* Get array of SSM params using aws-sdk | ||
* Throw error if SSM returns an error because we asked for params that don't exist | ||
* @throws {Error} When any invalid parameters found in response | ||
* @param {String[]} ssmParamNames Array of param names to fetch | ||
* @param {Function} getter Function that returns a promise which resolves with the params returned from ssm | ||
* @return {Promise.<Object[]>} Array of SSM params from aws-sdk | ||
*/ | ||
function getSSMParams (ssmParamNames) { | ||
// prevents throwing error from aws-sdk when empty params passed | ||
if (!ssmParamNames.length) { | ||
return Promise.resolve([]) | ||
const handleInvalidParams = ({ Parameters, InvalidParameters }) => { | ||
if (InvalidParameters && InvalidParameters.length) { | ||
throw new Error(`InvalidParameters present: ${InvalidParameters.join(', ')}`) | ||
} | ||
return ssmInstance.getParameters({Names: ssmParamNames, WithDecryption: true}) | ||
.promise() | ||
.then(({Parameters, InvalidParameters}) => { | ||
if (InvalidParameters && InvalidParameters.length) { | ||
throw new Error(`InvalidParameters present: ${InvalidParameters.join(', ')}`) | ||
} | ||
return Parameters | ||
}) | ||
return Parameters | ||
} | ||
/** | ||
* Assigns params from SSM response to target object using names from middleware options | ||
* @param {Object} paramsTarget Target object to assign params to | ||
* @param {Object} userParamsMap Options from middleware defining param names | ||
* @param {Object[]} ssmResponse Array of params returned from SSM by aws-sdk | ||
* Get object of user param names as keys and SSM param values as value | ||
* @param {Object} userParamsMap Params object from middleware options | ||
* @param {Object[]} ssmParams Array of parameters from SSM returned by aws-sdk | ||
* @return {Object} Merged object for assignment to target object | ||
*/ | ||
function assignSSMParamsToTarget (paramsTarget, userParamsMap, ssmResponse) { | ||
const paramsToAttach = getParamsToAssign(userParamsMap, ssmResponse) | ||
const getParamsToAssignByName = (userParamsMap, ssmParams) => { | ||
const ssmToUserParamsMap = invertObject(userParamsMap) | ||
Object.assign(paramsTarget, paramsToAttach) | ||
return ssmParams.reduce((aggregator, ssmParam) => { | ||
aggregator[ssmToUserParamsMap[ssmParam.Name]] = ssmParam.Value | ||
return aggregator | ||
}, {}) | ||
} | ||
@@ -124,29 +134,18 @@ | ||
* Get object of user param names as keys and SSM param values as value | ||
* @param {Object} userParamsMap Params object from middleware options | ||
* @param {String} userParamsPath Path string from middleware options | ||
* @param {Object[]} ssmParams Array of parameters from SSM returned by aws-sdk | ||
* @param {String} prefix String to prefix to param values from a given path | ||
* @param {Function} nameMapper function to build the local name for a param based on path, prefix, and name in SSM | ||
* @return {Object} Merged object for assignment to target object | ||
*/ | ||
function getParamsToAssign (userParamsMap, ssmParams) { | ||
const ssmToUserParamsMap = invertObject(userParamsMap) | ||
const targetObject = {} | ||
const getParamsToAssignByPath = (userParamsPath, ssmParams, prefix, nameMapper) => | ||
ssmParams.reduce((aggregator, ssmParam) => { | ||
aggregator[nameMapper(userParamsPath, ssmParam.Name, prefix)] = ssmParam.Value | ||
return aggregator | ||
}, {}) | ||
for (let {Name: ssmParamName, Value: ssmParamValue} of ssmParams) { | ||
const userParamName = ssmToUserParamsMap[ssmParamName] | ||
targetObject[userParamName] = ssmParamValue | ||
} | ||
return targetObject | ||
} | ||
function invertObject (obj) { | ||
const invertedObject = {} | ||
for (const key in obj) { | ||
if (obj.hasOwnProperty(key)) { | ||
invertedObject[obj[key]] = key | ||
} | ||
} | ||
return invertedObject | ||
} | ||
const invertObject = obj => | ||
Object.keys(obj).reduce((aggregator, key) => { | ||
aggregator[obj[key]] = key | ||
return aggregator | ||
}, {}) |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
122570
2.75%2886
1.62%629
-0.32%12
71.43%