Skip to content

Commit

Permalink
Merge pull request #1 from formspree/swap-env-vars
Browse files Browse the repository at this point in the history
Perform env var ref substitution
  • Loading branch information
derrickreimer committed Jul 13, 2020
2 parents 9404103 + 0fd5ce1 commit 5b1c9ea
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 2 deletions.
73 changes: 72 additions & 1 deletion src/cmds/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const version = require('../../package.json').version;
const log = require('../log');
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
Expand Down Expand Up @@ -144,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'
Expand All @@ -168,10 +177,72 @@ 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.
let undefinedEnvRefs = [];

const rawConfigWithSecrets = rawConfig.replace(
/\$([A-Za-z0-9_]+)/gi,
(_match, variableName) => {
let value = env[variableName];
if (value) return value;
undefinedEnvRefs.push(variableName);
}
);

if (undefinedEnvRefs.length > 0) {
log.error(
`The following environment variables were referenced but are not defined: ${undefinedEnvRefs.join(
', '
)}`
);

process.exitCode = 1;
return;
}

let config;

try {
config = JSON.parse(rawConfig);
config = JSON.parse(rawConfigWithSecrets);
} catch (err) {
log.error('Configuration could not be parsed');
process.exitCode = 1;
Expand Down
24 changes: 24 additions & 0 deletions src/traverse.js
Original file line number Diff line number Diff line change
@@ -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 };
72 changes: 72 additions & 0 deletions test/cmds/__snapshots__/deploy.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,75 @@ 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 [
"✔ Deployment succeeded (xxxx-xxxx-xxxx)",
],
]
`;
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 [
"✕ The following environment variables were referenced but are not defined: MY_SECRET_1, API_KEY_1",
],
]
`;
109 changes: 108 additions & 1 deletion test/cmds/deploy.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const deploy = require('@statickit/deploy');
const version = require('../../package.json').version;

jest.mock('process', () => ({ env: {} }));
jest.mock('process', () => ({
env: { MY_SECRET: 'pa$$w0rd', API_KEY: '12345' }
}));
jest.mock('@statickit/deploy');
jest.mock('../../src/shim');

Expand All @@ -24,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 () => {
Expand Down Expand Up @@ -190,3 +193,107 @@ it('displays general validation errors', async () => {
await cmd.handler({ config: '{}', key: 'xxx' });
expect(console.error.mock.calls).toMatchSnapshot();
});

it('substitutes all referenced environment variables', async () => {
deploy.request.mockImplementation(params => {
expect(params.config.mySecret).toBe('pa$$w0rd');
expect(params.config.apiKey).toBe('12345');
return Promise.resolve({
status: 200,
data: { id: 'xxxx-xxxx-xxxx', shim: null }
});
});

const config = {
mySecret: '$MY_SECRET',
apiKey: '$API_KEY'
};

await cmd.handler({ config: JSON.stringify(config), key: 'xxx' });
expect(console.log.mock.calls).toMatchSnapshot();
});

it('throws an error if undefined env vars are referenced', async () => {
const config = {
mySecret: '$MY_SECRET_1',
apiKey: '$API_KEY_1'
};

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();
});

0 comments on commit 5b1c9ea

Please sign in to comment.