Skip to content

Commit

Permalink
Add in RDS Auth Token option to dbManager (#462)
Browse files Browse the repository at this point in the history
* feat: add in RDS Auth Token option

Closes #428

* fix: mock out RDS sdk

* fix: remove async due to causing multiple `next()` calls

* fix: bring back async without next()

* fix: remove unused next

* version bump

Co-authored-by: Luciano Mammino <lucianomammino@gmail.com>
  • Loading branch information
willfarrell and lmammino committed Jan 9, 2020
1 parent 17a853a commit b29d582
Show file tree
Hide file tree
Showing 55 changed files with 275 additions and 152 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
"version": "1.0.0-beta.1"
"version": "1.0.0-beta.2"
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "middy-monorepo",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "🛵 The stylish Node.js middleware engine for AWS Lambda",
"engines": {
"node": ">=10"
Expand Down
8 changes: 4 additions & 4 deletions packages/cache/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/cache/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@middy/cache",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "Cache middleware for the middy framework",
"engines": {
"node": ">=10"
Expand Down Expand Up @@ -41,7 +41,7 @@
"@middy/core": ">=1.0.0-alpha"
},
"devDependencies": {
"@middy/core": "^1.0.0-beta.1",
"@middy/core": "^1.0.0-beta.2",
"es6-promisify": "^6.0.2"
},
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@middy/core",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "🛵 The stylish Node.js middleware engine for AWS Lambda (core package)",
"engines": {
"node": ">=10"
Expand Down
48 changes: 47 additions & 1 deletion packages/db-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ npm install --save @middy/db-manager
- `secretsPath` (optional): if for any reason you want to pass credentials using context, pass path to secrets laying in context object - good example is combining this middleware with [ssm](#ssm)
- `removeSecrets` (optional): By default is true. Works only in combination with `secretsPath`. Removes sensitive data from context once client is initialized.
- `forceNewConnection` (optional): Creates new connection on every run and destroys it after. Database client needs to have `destroy` function in order to properly clean up connections.
- `rdsSigner` (optional): Will use to create an IAM RDS Auth Token for the database connection using `[RDS.Signer](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS/Signer.html)`. See AWs docs for required params, `region` is automatically pulled from the `hostname` unless overridden.

## Sample usage

Expand Down Expand Up @@ -128,7 +129,7 @@ handler.use(secretsManager({
throwOnFailedCall: true
}));
handler.use(dbManager({
client: knex
client: knex,
config: {
client: 'pg',
connection: {
Expand All @@ -140,6 +141,51 @@ handler.use(dbManager({
}));
```

Connect to RDS using IAM Auth Tokens and TLS

```javascript
const tls = require('tls')
const ca = require('fs').readFileSync(`${__dirname}/rds-ca-2019-root.pem`) // Download from https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html

const handler = middy(async (event, context) => {
const { db } = context;
const records = await db.select('*').from('my_table');
console.log(records);
});
handler.use(dbManager({
rdsSigner:{
region: 'us-east-1',
hostname: '*****.******.{region}.rds.amazonaws.com',
username: 'iam_user',
database: 'myapp_test',
port: '5432'
},
secretsPath: 'password',
config: {
client: 'pg',
connection: {
host: '*****.******.{region}.rds.amazonaws.com',
user: 'your_database_user',
database: 'myapp_test',
port: '5432',
ssl: {
rejectUnauthorized: true,
ca,
checkServerIdentity: (host, cert) => {
const error = tls.checkServerIdentity(host, cert)
if (error && !cert.subject.CN.endsWith('.rds.amazonaws.com')) {
return error
}
}
}
}
}
}));
```

**Note:** If you see an error

See AWS Docs [Rotating Your SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html) to ensure you're using the right certificate.

## Middy documentation and examples

Expand Down
59 changes: 57 additions & 2 deletions packages/db-manager/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
jest.mock('knex')

const knex = require('knex')
const RDS = require('aws-sdk/clients/rds')

const middy = require('../../core')
const dbManager = require('../')

describe('💾 Database manager', () => {
let destroyFn
let clientMock

let getAuthTokenMock
let SignerMock

beforeEach(() => {
getAuthTokenMock = jest.fn()
SignerMock = jest.fn(() => ({
getAuthToken: getAuthTokenMock
}))
RDS.prototype.Signer = SignerMock
destroyFn = jest.fn()
clientMock = jest.fn(() => ({
destroy: destroyFn
Expand Down Expand Up @@ -44,6 +54,7 @@ describe('💾 Database manager', () => {
})
})

// TODO async before causing quite error w/r toHaveBeenCalledTimes
test('it should destroy instance if forceNewConnection flag provided', (done) => {
knex.mockReturnValue(clientMock())
const handler = middy((event, context, cb) => {
Expand Down Expand Up @@ -233,8 +244,7 @@ describe('💾 Database manager', () => {
user: '1234',
password: '56678'
}
const config = {
}
const config = { connection }
const handler = middy((event, context, cb) => {
expect(context.db.toString()).toEqual(clientMock.toString()) // compare invocations, not functions
expect(newClient).toHaveBeenCalledTimes(1)
Expand Down Expand Up @@ -275,4 +285,49 @@ describe('💾 Database manager', () => {
done()
})
})

// TODO async before causing quite error w/r toHaveBeenCalledTimes, running test by itself passes.
test('it should create an authToken to be used as the password', async () => {
const newClient = jest.fn().mockReturnValue(clientMock)
const secretsPath = 'secret_location'
const secretsValue = 'secret_token'
const config = {
connection: {
host: '127.0.0.1',
user: '1234',
port: '5432'
}
}
getAuthTokenMock.mockReturnValue({
promise: () => Promise.resolve(secretsValue)
})
const handler = middy((event, context, cb) => {
expect(context.db.toString()).toEqual(clientMock.toString()) // compare invocations, not functions
expect(newClient).toHaveBeenCalledTimes(1)
config.connection[secretsPath] = secretsValue
expect(newClient).toHaveBeenCalledWith(config)
return cb(null, event.body) // propagates the body as a response
})

handler.use(dbManager({
client: newClient,
rdsSigner: {
region: 'us-east-1',
hostname: '127.0.0.1',
username: '1234',
port: '5432'
},
secretsPath,
config
}))

// invokes the handler
const event = {
body: JSON.stringify({ foo: 'bar' })
}
await handler(event, {}, (err, body) => {
expect(err).toEqual(null)
expect(body).toEqual('{"foo":"bar"}')
})
})
})
1 change: 1 addition & 0 deletions packages/db-manager/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Knex from 'knex';

export interface DbManagerOptions {
client?: any,
rdsSigner?: any,
config: Knex.Config | Knex.AliasDict,
forceNewConnection?: boolean,
secretsPath?: string,
Expand Down
33 changes: 27 additions & 6 deletions packages/db-manager/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
const knex = require('knex')
const RDS = require('aws-sdk/clients/rds')

let dbInstance

module.exports = (opts) => {
const defaults = {
client: knex,
config: null,
rdsSigner: null,
forceNewConnection: false,
secretsPath: null, // provide path where credentials lay in context
secretsPath: null, // provide path where credentials lay in context, default to try to get RDS authToken
removeSecrets: true
}

const options = Object.assign({}, defaults, opts)

function cleanup (handler, next) {
const cleanup = (handler, next) => {
if (options.forceNewConnection && (dbInstance && typeof dbInstance.destroy === 'function')) {
dbInstance.destroy((err) => next(err || handler.error))
}
next(handler.error)
}

const signer = (config) => {
if (typeof config.port === 'string') config.port = Number.parseInt(config.port)
const signer = new RDS.Signer(config)
return new Promise((resolve, reject) => {
signer.getAuthToken({}, (err, token) => {
if (err) {
reject(err)
}
resolve(token)
})
})
}

return {
before: (handler, next) => {
before: async (handler) => {
const {
client,
config,
Expand All @@ -33,18 +48,24 @@ module.exports = (opts) => {
if (!config) {
throw new Error('Config is required in dbManager')
}

if (!dbInstance || forceNewConnection) {
if (secretsPath) {
config.connection = Object.assign({}, config.connection || {}, handler.context[secretsPath])
const secrets = {}

if (options.rdsSigner && secretsPath) {
secrets[secretsPath] = await signer(options.rdsSigner)
} else if (secretsPath) {
secrets[secretsPath] = handler.context[secretsPath]
}
config.connection = Object.assign({}, config.connection || {}, secrets)

dbInstance = client(config)
}

Object.assign(handler.context, { db: dbInstance })
if (secretsPath && removeSecrets) {
delete handler.context[secretsPath]
}
return next()
},

after: cleanup,
Expand Down
8 changes: 4 additions & 4 deletions packages/db-manager/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/db-manager/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@middy/db-manager",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "Simple database manager for the middy framework",
"engines": {
"node": ">=10"
Expand Down Expand Up @@ -40,7 +40,7 @@
"knex": "^0.17.3"
},
"devDependencies": {
"@middy/core": "^1.0.0-beta.1"
"@middy/core": "^1.0.0-beta.2"
},
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
}
8 changes: 4 additions & 4 deletions packages/do-not-wait-for-empty-event-loop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/do-not-wait-for-empty-event-loop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@middy/do-not-wait-for-empty-event-loop",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "Middleware for the middy framework that allows to easily disable the wait for empty event loop in a Lambda function",
"engines": {
"node": ">=10"
Expand Down Expand Up @@ -41,7 +41,7 @@
"@middy/core": ">=1.0.0-alpha"
},
"devDependencies": {
"@middy/core": "^1.0.0-beta.1",
"@middy/core": "^1.0.0-beta.2",
"es6-promisify": "^6.0.2"
},
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
Expand Down
Loading

0 comments on commit b29d582

Please sign in to comment.