Skip to content

Commit

Permalink
Merge pull request #25 from hanswesterbeek/add-iam-auth-support
Browse files Browse the repository at this point in the history
Add iam auth support
  • Loading branch information
jameshy committed Apr 6, 2021
2 parents e31086b + 2afa594 commit e9fb1ef
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -36,3 +36,6 @@ jspm_packages
# Optional REPL history
.node_repl_history
*.zip

# IntelliJ files
.idea/
19 changes: 19 additions & 0 deletions README.md
Expand Up @@ -83,6 +83,25 @@ openssl enc -aes-256-cbc -d \
-iv $(< postgres-27-12-2019@13-19-13.backup.iv)
```

#### IAM-based Postgres authentication

Your context may require that you use IAM-based authentication to log into the Postgres service.
Support for this can be enabled my making your Cloudwatch Event look like this.

```json

{
"PGDATABASE": "dbname",
"PGUSER": "postgres",
"USE_IAM_AUTH": true,
"PGHOST": "host",
"S3_BUCKET" : "db-backups",
"ROOT": "hourly-backups"
}
```

If you supply `USE_IAM_AUTH` with a value of `true`, the `PGPASSWORD` var may be omitted in the CloudWatch event.
If you still provide it, it will be ignored.

## Developer

Expand Down
3 changes: 2 additions & 1 deletion lib/config.js
Expand Up @@ -5,5 +5,6 @@ module.exports = {
S3_REGION: 'eu-west-1',
PGDUMP_PATH: path.join(__dirname, '../bin/postgres-11.6'),
// maximum time allowed to connect to postgres before a timeout occurs
PGCONNECT_TIMEOUT: 15
PGCONNECT_TIMEOUT: 15,
USE_IAM_AUTH: false
}
4 changes: 3 additions & 1 deletion lib/handler.js
@@ -1,6 +1,7 @@
const utils = require('./utils')
const uploadS3 = require('./upload-s3')
const pgdump = require('./pgdump')
const decorateWithIamToken = require('./iam')
const encryption = require('./encryption')

const DEFAULT_CONFIG = require('./config')
Expand Down Expand Up @@ -33,7 +34,8 @@ async function backup(config) {
}

async function handler(event) {
const config = { ...DEFAULT_CONFIG, ...event }
const baseConfig = { ...DEFAULT_CONFIG, ...event }
const config = event.USE_IAM_AUTH === true ? decorateWithIamToken(baseConfig) : baseConfig
try {
return await backup(config)
}
Expand Down
14 changes: 14 additions & 0 deletions lib/iam.js
@@ -0,0 +1,14 @@
const AWS = require('aws-sdk')

function decorateWithIamToken(baseConfig) {
const rdsSigner = new AWS.RDS.Signer()
const token = rdsSigner.getAuthToken({
hostname: baseConfig.PGHOST,
port: baseConfig.PGPORT != null ? baseConfig.PGPORT : 5432,
region: baseConfig.S3_REGION,
username: baseConfig.PGUSER
})
return { ...baseConfig, PGPASSWORD: token }
}

module.exports = decorateWithIamToken
124 changes: 124 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -9,6 +9,7 @@
},
"devDependencies": {
"aws-sdk": "2.595.0",
"aws-sdk-mock": "5.1.0",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"coveralls": "3.0.9",
Expand Down
3 changes: 3 additions & 0 deletions test/.eslintrc
Expand Up @@ -4,5 +4,8 @@
"globals": {
"describe": true,
"it": true
},
"env": {
"mocha": true
}
}
20 changes: 20 additions & 0 deletions test/handler.js
Expand Up @@ -6,7 +6,10 @@ const mockDate = require('mockdate')
const mockSpawn = require('mock-spawn')
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const AWSMOCK = require('aws-sdk-mock')
const AWS = require('aws-sdk')

AWSMOCK.setSDKInstance(AWS)
chai.should()
chai.use(chaiAsPromised)

Expand Down Expand Up @@ -69,6 +72,23 @@ describe('Handler', () => {
)
})

it('should be able to authenticate via IAM ', async () => {
const { s3Spy, pgSpy } = makeMockHandler()

const iamMockEvent = { ...mockEvent, USE_IAM_AUTH: true }
const token = 'foo'
AWSMOCK.mock('RDS.Signer', 'getAuthToken', token)
await handler(iamMockEvent)
// handler should have called pgSpy with correct arguments
expect(pgSpy.calledOnce).to.be.true
expect(s3Spy.calledOnce).to.be.true
expect(s3Spy.firstCall.args).to.have.length(3)
const config = s3Spy.firstCall.args[1]
// production code is synchronous, so this is annoying
expect(await config.PGPASSWORD.promise()).to.equal(token)
AWSMOCK.restore('RDS')
})

it('should upload the backup file and an iv file', async () => {
const { s3Spy } = makeMockHandler()

Expand Down
41 changes: 41 additions & 0 deletions test/iam.js
@@ -0,0 +1,41 @@
/* eslint no-underscore-dangle: 0 */
const { expect } = require('chai')
const rewire = require('rewire')
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const AWSMOCK = require('aws-sdk-mock')
const AWS = require('aws-sdk')

chai.should()
chai.use(chaiAsPromised)

AWSMOCK.setSDKInstance(AWS)


const decorateWithIamToken = rewire('../lib/iam')

describe('iam-based auth', () => {
it('should set the postgres default if no PGPORT is set', async () => {
const mockEvent = { USE_IAM_AUTH: true }
const token = 'foo'
AWSMOCK.mock('RDS.Signer', 'getAuthToken', (options) => {
expect(options.port).to.equal(5432)
return token
})
decorateWithIamToken(mockEvent)
})

it('should apply PGPORT to the auth-token request', async () => {
const mockEvent = { USE_IAM_AUTH: true, PGPORT: 2345 }
const token = 'foo'
AWSMOCK.mock('RDS.Signer', 'getAuthToken', (options) => {
expect(options.port).to.equal(mockEvent.PGPORT)
return token
})
decorateWithIamToken(mockEvent)
})

afterEach(() => {
AWSMOCK.restore('RDS.Signer')
})
})

0 comments on commit e9fb1ef

Please sign in to comment.