diff --git a/src/cmds/deploy.js b/src/cmds/deploy.js index 459c52a..68ee9a7 100644 --- a/src/cmds/deploy.js +++ b/src/cmds/deploy.js @@ -7,6 +7,7 @@ const messages = require('../messages'); const shim = require('../shim'); const env = require('process').env; const { stripIndent } = require('common-tags'); +const { traverse } = require('../traverse'); const indent = (text, depth = 2) => { return text @@ -145,6 +146,13 @@ exports.builder = yargs => { describe: 'API endpoint' }); + yargs.option('force', { + alias: 'f', + describe: 'Skip verifying that secrets reference environment variables', + type: 'boolean', + default: false + }); + yargs.option('file', { describe: 'Path to the local `statickit.json` file', default: 'statickit.json' @@ -169,6 +177,43 @@ exports.handler = async args => { return; } + let parsedRawConfig; + + try { + parsedRawConfig = JSON.parse(rawConfig); + } catch (err) { + log.error('Configuration could not be parsed'); + process.exitCode = 1; + return; + } + + // Traverse the config and validate that certain specially-named keys + // reference environment variables. + let invalidKeys = []; + const sensitiveKeys = ['apiKey', 'apiSecret', 'secretKey']; + + traverse(parsedRawConfig, (key, value) => { + if ( + sensitiveKeys.indexOf(key) > -1 && + !value.match(/^\$([A-Za-z0-9_]+)$/) + ) { + invalidKeys.push(key); + } + }); + + if (!args.force && invalidKeys.length > 0) { + log.error( + `The following properties must reference environment variables: ${invalidKeys.join( + ', ' + )}` + ); + + log.meta('To override this, use the `-f` flag.'); + + process.exitCode = 1; + return; + } + // Replace environment variable $-references with the actual values // If the environment variable is not defined, store in an array an present // an error to the user. diff --git a/src/traverse.js b/src/traverse.js new file mode 100644 index 0000000..c0e2ef5 --- /dev/null +++ b/src/traverse.js @@ -0,0 +1,24 @@ +function isArray(o) { + return Object.prototype.toString.call(o) === '[object Array]'; +} + +function traverse(obj, fn) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + let value = obj[key]; + + if (isArray(value)) { + value.forEach(element => { + if (typeof element === 'object' && element !== null) + traverse(element, fn); + }); + } else if (typeof value === 'object' && value !== null) { + traverse(value, fn); + } else { + fn(key, value); + } + } + } +} + +module.exports = { traverse }; diff --git a/test/cmds/__snapshots__/deploy.test.js.snap b/test/cmds/__snapshots__/deploy.test.js.snap index 0127b5e..4b6c651 100644 --- a/test/cmds/__snapshots__/deploy.test.js.snap +++ b/test/cmds/__snapshots__/deploy.test.js.snap @@ -182,6 +182,14 @@ Array [ ] `; +exports[`skips validating inline secrets with force flag 1`] = ` +Array [ + Array [ + "✔ Deployment succeeded (xxxx-xxxx-xxxx)", + ], +] +`; + exports[`substitutes all referenced environment variables 1`] = ` Array [ Array [ @@ -190,6 +198,54 @@ Array [ ] `; +exports[`throws an error if apiKey properties do not point to env vars 1`] = ` +Array [ + Array [ + "✕ The following properties must reference environment variables: apiKey", + ], +] +`; + +exports[`throws an error if apiKey properties do not point to env vars 2`] = ` +Array [ + Array [ + "> To override this, use the \`-f\` flag.", + ], +] +`; + +exports[`throws an error if apiSecret properties do not point to env vars 1`] = ` +Array [ + Array [ + "✕ The following properties must reference environment variables: apiSecret", + ], +] +`; + +exports[`throws an error if apiSecret properties do not point to env vars 2`] = ` +Array [ + Array [ + "> To override this, use the \`-f\` flag.", + ], +] +`; + +exports[`throws an error if secretKey properties do not point to env vars 1`] = ` +Array [ + Array [ + "✕ The following properties must reference environment variables: secretKey", + ], +] +`; + +exports[`throws an error if secretKey properties do not point to env vars 2`] = ` +Array [ + Array [ + "> To override this, use the \`-f\` flag.", + ], +] +`; + exports[`throws an error if undefined env vars are referenced 1`] = ` Array [ Array [ diff --git a/test/cmds/deploy.test.js b/test/cmds/deploy.test.js index 887591e..0e75be7 100644 --- a/test/cmds/deploy.test.js +++ b/test/cmds/deploy.test.js @@ -26,6 +26,7 @@ afterEach(() => { console.log.mockRestore(); console.error.mockRestore(); shim.install.mockReset(); + deploy.request.mockReset(); }); it('sends a deploy request with the right params', async () => { @@ -221,3 +222,78 @@ it('throws an error if undefined env vars are referenced', async () => { await cmd.handler({ config: JSON.stringify(config), key: 'xxx' }); expect(console.error.mock.calls).toMatchSnapshot(); }); + +it('throws an error if apiKey properties do not point to env vars', async () => { + const config = { + forms: { + contactForm: { + actions: [ + { + apiKey: 'my-inline-key' + } + ] + } + } + }; + + await cmd.handler({ config: JSON.stringify(config), key: 'xxx' }); + expect(console.error.mock.calls).toMatchSnapshot(); + expect(console.log.mock.calls).toMatchSnapshot(); +}); + +it('throws an error if apiSecret properties do not point to env vars', async () => { + const config = { + forms: { + contactForm: { + actions: [ + { + apiSecret: 'my-inline-key' + } + ] + } + } + }; + + await cmd.handler({ config: JSON.stringify(config), key: 'xxx' }); + expect(console.error.mock.calls).toMatchSnapshot(); + expect(console.log.mock.calls).toMatchSnapshot(); +}); + +it('throws an error if secretKey properties do not point to env vars', async () => { + const config = { + forms: { + contactForm: { + actions: [ + { + secretKey: 'my-inline-key' + } + ] + } + } + }; + + await cmd.handler({ config: JSON.stringify(config), key: 'xxx' }); + expect(console.error.mock.calls).toMatchSnapshot(); + expect(console.log.mock.calls).toMatchSnapshot(); +}); + +it('skips validating inline secrets with force flag', async () => { + const config = { + apiKey: 'my-inline-key' + }; + + deploy.request.mockImplementation(params => { + expect(params.config.apiKey).toBe('my-inline-key'); + return Promise.resolve({ + status: 200, + data: { id: 'xxxx-xxxx-xxxx', shim: null } + }); + }); + + await cmd.handler({ + config: JSON.stringify(config), + key: 'xxx', + force: true + }); + expect(console.log.mock.calls).toMatchSnapshot(); +});