From 0f14bf34993fe1f01adbfd6b8be544130b1b2b9c Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 24 Apr 2017 23:02:08 -0700 Subject: [PATCH] add PRIVATE_KEY_PATH env var support; closes #122 The loading preference is: 1. CLI flag 2. `PRIVATE_KEY` 3. `PRIVATE_KEY_PATH` 4. Find a `.pem` file in the current working dir Changes: - break out PK loading logic into module `lib/private-key.js` - add more info to error messaging if probot fails to find a PK - add tests - add note to deployment docs --- bin/probot-run | 22 +-------- docs/deployment.md | 8 +++- lib/private-key.js | 37 +++++++++++++++ test/private-key.js | 107 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 lib/private-key.js create mode 100644 test/private-key.js diff --git a/bin/probot-run b/bin/probot-run index 77123bb5..62a0f63a 100755 --- a/bin/probot-run +++ b/bin/probot-run @@ -3,20 +3,14 @@ require('dotenv').config(); const program = require('commander'); +const {findPrivateKey} = require('../lib/private-key'); program .usage('[options] ') .option('-i, --integration ', 'ID of the GitHub Integration', process.env.INTEGRATION_ID) .option('-s, --secret ', 'Webhook secret of the GitHub Integration', process.env.WEBHOOK_SECRET || 'development') .option('-p, --port ', 'Port to start the server on', process.env.PORT || 3000) - .option('-P, --private-key ', 'Path to certificate of the GitHub Integration', path => { - try { - return require('fs').readFileSync(path); - } catch (err) { - console.warn(err.message); - process.exit(1); - } - }, process.env.PRIVATE_KEY) + .option('-P, --private-key ', 'Path to certificate of the GitHub Integration', findPrivateKey) .option('-t, --tunnel ', 'Expose your local bot to the internet', process.env.SUBDOMAIN || process.env.NODE_ENV != 'production') .parse(process.argv); @@ -25,18 +19,6 @@ if(!program.integration) { program.help(); } -if(!program.privateKey) { - const fs = require('fs'); - const key = fs.readdirSync(process.cwd()).find(path => path.endsWith('.pem')); - - if (key) { - program.privateKey = fs.readFileSync(key); - } else { - console.warn("Missing GitHub Integration private key.\nUse --private-key flag or set PRIVATE_KEY environment variable."); - process.exit(1); - } -} - if(program.tunnel) { try { setupTunnel(); diff --git a/docs/deployment.md b/docs/deployment.md index 160a4d7e..3013cc1b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -32,7 +32,13 @@ To deploy a plugin to any cloud provider, you will need 3 environment variables: - `INTEGRATION_ID`: the ID of the integration, which you can get from the [integration settings page](https://github.com/settings/integrations). - `WEBHOOK_SECRET`: the **Webhook Secret** that you generated when you created the integration. -- `PRIVATE_KEY`: the contents of the private key you downloaded after creating the integration. + +And one of: + +- `PRIVATE_KEY`: the contents of the private key you downloaded after creating the integration, OR... +- `PRIVATE_KEY_PATH`: the path to a private key file. + +`PRIVATE_KEY` takes precedence over `PRIVATE_KEY_PATH`. ### Heroku diff --git a/lib/private-key.js b/lib/private-key.js new file mode 100644 index 00000000..8f7f0a57 --- /dev/null +++ b/lib/private-key.js @@ -0,0 +1,37 @@ +const fs = require('fs'); + +/** + * Finds a private key through various user-(un)specified methods. + * Order of precedence: + * 1. Explicit path (CLI option) + * 2. `PRIVATE_KEY` env var + * 3. `PRIVATE_KEY_PATH` env var + * 4. Any file w/ `.pem` extension in current working dir + * @param {string} [filepath] - Explicit, user-defined path to keyfile + * @returns {string} Private key + */ +function findPrivateKey(filepath) { + if (filepath) { + return fs.readFileSync(filepath); + } + if (process.env.PRIVATE_KEY) { + return process.env.PRIVATE_KEY; + } + if (process.env.PRIVATE_KEY_PATH) { + return fs.readFileSync(process.env.PRIVATE_KEY_PATH); + } + const foundPath = fs.readdirSync(process.cwd()) + .find(path => path.endsWith('.pem')); + if (foundPath) { + return findPrivateKey(foundPath); + } + throw new Error(`Missing private key for GitHub Integration. Please use: + * \`--private-key=/path/to/private-key\` flag, or + * \`PRIVATE_KEY\` environment variable, or + * \`PRIVATE_KEY_PATH\` environment variable +`); +} + +module.exports = { + findPrivateKey +}; diff --git a/test/private-key.js b/test/private-key.js new file mode 100644 index 00000000..eebbbab1 --- /dev/null +++ b/test/private-key.js @@ -0,0 +1,107 @@ +const fs = require('fs'); + +const expect = require('expect'); + +const {findPrivateKey} = require('../lib/private-key'); + +describe('private-key', function () { + let privateKey; + let keyfilePath; + + beforeEach(function () { + privateKey = 'I AM PRIVET KEY!?!!~1!'; + keyfilePath = '/some/path'; + expect.spyOn(fs, 'readFileSync') + .andReturn(privateKey); + }); + + afterEach(function () { + expect.restoreSpies(); + }); + + describe('findPrivateKey()', function () { + describe('when a filepath is provided', function () { + it('should read the file at given filepath', function () { + findPrivateKey(keyfilePath); + expect(fs.readFileSync) + .toHaveBeenCalledWith(keyfilePath); + }); + + it('should return the key', function () { + expect(findPrivateKey(keyfilePath)) + .toEqual(privateKey); + }); + }); + + describe('when a PRIVATE_KEY env var is provided', function () { + beforeEach(function () { + process.env.PRIVATE_KEY = privateKey; + }); + + afterEach(function () { + delete process.env.PRIVATE_KEY; + }); + + it('should return the key', function () { + expect(findPrivateKey()) + .toEqual(privateKey); + }); + }); + + describe('when a PRIVATE_KEY_PATH env var is provided', function () { + beforeEach(function () { + process.env.PRIVATE_KEY_PATH = keyfilePath; + }); + + afterEach(function () { + delete process.env.PRIVATE_KEY_PATH; + }); + + it('should read the file at given filepath', function () { + findPrivateKey(); + expect(fs.readFileSync) + .toHaveBeenCalledWith(keyfilePath); + }); + + it('should return the key', function () { + expect(findPrivateKey()) + .toEqual(privateKey); + }); + }); + + describe('when no private key is provided', function () { + beforeEach(function () { + expect.spyOn(fs, 'readdirSync') + .andReturn([ + 'foo.txt', + 'foo.pem' + ]); + }); + + it('should look for one in the current directory', function () { + findPrivateKey(); + expect(fs.readdirSync) + .toHaveBeenCalledWith(process.cwd()); + }); + + describe('and a key file is present', function () { + it('should load the key file', function () { + findPrivateKey(); + expect(fs.readFileSync) + .toHaveBeenCalledWith('foo.pem'); + }); + }); + + describe('and a key file is not present', function () { + beforeEach(function () { + fs.readdirSync.restore(); + }); + + it('should throw an error', function () { + expect(findPrivateKey) + .toThrow(Error, /missing private key for github integrationy/i); + }); + }); + }); + }); +});