Skip to content

Commit

Permalink
feat(middleware): add AWS Secrets Manager middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Lansoweb committed Sep 9, 2019
1 parent a0e34b5 commit dc54048
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 3 deletions.
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ decorates the output with HAL links for the entity and a custom middleware ```ge
### How it works

This module implements the middleware pipeline in a onion style.

The terminology "pipeline" is often used to describe the onion. One way of looking at the "onion" is as a queue, which is first-in-first-out (FIFO) in operation. This means that the first middleware on the queue is executed first, and this invokes the next, and so on (and hence the "next" terminology)

Each middleware receives the event and context, and pass to the handler only when wanting to hand off processing.

A middleware can return a response immediately, for example validate in input before handling it to the handler. In this case, the pipeline will not continue and the request will not even reach the handler.
A middleware can return a response immediately, for example validate in input before handling it to the handler. In this case, the pipeline will not continue and the request will not even reach the handler.

### Pipeline

Expand Down Expand Up @@ -89,11 +89,43 @@ Will fetch the keys from SSM Parameter Store, embed in the destination and cache
* ssmOptions: Will be passed to the SSM constructor
* expiryMs: Will keep the parameters in the memory cache for expiryMs in milliseconds. Default is 10 minutes.


#### secretsManager(keyMap, destination, { secretsManagerOptions: {}, expiryMs: 10 * 60 * 1000 })
Will fetch the keys from AWS Secrets Manager, embed in the destination and cache the result for the specified expiryMs.
* keyMap: A map of key names and paths in Secrets Manager. Example:
{
MYSQL_HOST: '/production/MYSQL_HOST',
MYSQL_USER: '/production/MYSQL_USER',
MYSQL_PASS: '/production/MYSQL_PASS',
}
* destination: the destination of the parameters. Example: ```context``` . context will be used if null
* secretsManagerOptions: Will be passed to the AWS.secretsmanager constructor
* expiryMs: Will keep the parameters in the memory cache for expiryMs in milliseconds. Default is 10 minutes.

#### Custom
You can create a custom middleware and even call another middleware, just follow the following signature and example:
```javascript
const middleware = require('../lambda-middleware');

module.exports = () => {
return async (event, context, next) => {
if (process.env.NODE_ENV !== 'development') {
const params = {};
await secretManager({ MYSQL: 'prod/mysql' })(event, params, (ev, ctx) => {
process.env.MYSQL_HOST = ctx.MYSQL.host;
process.env.MYSQL_USER = ctx.MYSQL.username;
process.env.MYSQL_PASS = ctx.MYSQL.password;
});
}

return next(event, context);
};
};
```

```javascript
const middleware = require('../lambda-middleware');

module.exports = () => {
return async (event, context, next) => {
if (process.env.NODE_ENV !== 'development') {
Expand All @@ -112,4 +144,4 @@ module.exports = () => {
};
```

***Warning*** Don't forget to call ```return next(event, context)``` at the end or the pipeline will not continue and will get no response.
***Warning*** Don't forget to call ```return next(event, context)``` at the end or the pipeline will not continue and will get no response.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ module.exports = {
doNotWaitEmptyEventLoop: require('./src/middlewares/doNotWaitEmptyEventLoop'),
halResponse: require('./src/middlewares/halResponse'),
parameterStore: require('./src/middlewares/parameterStore'),
secretsManager: require('./src/middlewares/secretsManager'),
};
80 changes: 80 additions & 0 deletions src/middlewares/secretsManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable no-param-reassign,import/no-extraneous-dependencies */
const { promisify } = require('util');
const SecretsManager = require('aws-sdk/clients/secretsmanager');
const debug = require('debug')('lambda-middleware');

let secretsManager;
const cache = {
expiration: new Date(0),
items: {},
};

/**
* @param {Object} keyMap
* @param {Object} destination
* @param {{
* secretsManagerOptions: Object,
* expiryMs: integer
* }} incomingOptions
* @returns {function(*=, *=, *): *}
*/
module.exports = (keyMap, destination, incomingOptions) => {
const options = {
secretsManagerOptions: {},
expiryMs: 10 * 60 * 1000, // default expiry is 10 mins
...incomingOptions,
};

if (!keyMap) {
throw new Error('You need to provide a keys map');
}

if (options.expiryMs <= 0) {
throw new Error('You need to specify an expiry (ms) greater than 0, or leave it undefined');
}

const vars = Object.keys(keyMap);

const createSecretsManagerClient = () => {
if (secretsManager !== null) {
secretsManager = new SecretsManager(options.secretsManagerOptions);
// noinspection JSValidateTypes
secretsManager.getSecretValue = promisify(secretsManager.getSecretValue);
}
};

const reload = async () => {
debug(`Loading cache keys: ${vars}`);

createSecretsManagerClient();

cache.items = {};
await Promise.all(
vars.map(async key => {
const resp = await secretsManager.getSecretValue({ SecretId: keyMap[key] });
const secret = JSON.parse(resp.SecretString || '{}');
cache.items[key] = secret;
}),
);

debug(`Successfully loaded cache keys: ${vars}`);
const now = new Date();

cache.expiration = new Date(now.getTime() + options.expiryMs);
};

return async (event, context, next) => {
const now = new Date();
if (now > cache.expiration) {
await reload();
}

if (typeof destination !== 'object' || destination === null) {
Object.assign(context, cache.items);
} else {
Object.assign(destination, cache.items);
}

return next(event, context);
};
};

0 comments on commit dc54048

Please sign in to comment.