Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move config to config/content-security-policy.js #104

Merged
merged 3 commits into from
Aug 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 25 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ember install ember-cli-content-security-policy

## Configuration

This addon is configured via your application's `ember-cli-build.js` file using `ember-cli-content-security-policy` key:
This addon is configured via `config/content-security-policy.js` file.

- `delivery: string[]`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about including a typescript style interface here instead of this list? I did something similar over in ember-cli-babel's README, and I personally find it easier to follow than this bulleted list format.

Sorry for the noise in this issue though, we can do whatever changes we want to this bit of the README separately from your PR.

CSP is delivered via HTTP Header if delivery includes `"header"` and via meta element if it includes `"meta"`.
Expand Down Expand Up @@ -59,32 +59,30 @@ This addon is configured via your application's `ember-cli-build.js` file using
If your site uses **Google Fonts**, **Mixpanel**, a custom API at **custom-api.local** and you want to deliver the CSP using a meta element:

```js
module.exports = function(defaults) {
let app = new EmberApp(defaults, {
'ember-cli-content-security-policy': {
delivery: ['meta'],
policy: {
// Deny everything by default
'default-src': ["'none'"],
// Allow scripts at https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js
'script-src': ["'self'", "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"],
// Allow fonts to be loaded from http://fonts.gstatic.com
'font-src': ["'self'", "http://fonts.gstatic.com"],
// Allow data (xhr/websocket) from api.mixpanel.com and custom-api.local
'connect-src': ["'self'", "https://api.mixpanel.com", "https://custom-api.local"],
// Allow images from the origin itself (i.e. current domain)
'img-src': ["'self'"],
// Allow CSS loaded from https://fonts.googleapis.com
'style-src': ["'self'", "https://fonts.googleapis.com"],
// Omit `media-src` from policy
// Browser will fallback to default-src for media resources (which is 'none', see above)
'media-src': null
},
reportOnly: false
}
});

return app.toTree();
// config/content-security-policy.js

module.exports = function(environment) {
jelhan marked this conversation as resolved.
Show resolved Hide resolved
return {
delivery: ['meta'],
policy: {
// Deny everything by default
'default-src': ["'none'"],
// Allow scripts at https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js
'script-src': ["'self'", "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"],
// Allow fonts to be loaded from http://fonts.gstatic.com
'font-src': ["'self'", "http://fonts.gstatic.com"],
// Allow data (xhr/websocket) from api.mixpanel.com and custom-api.local
'connect-src': ["'self'", "https://api.mixpanel.com", "https://custom-api.local"],
// Allow images from the origin itself (i.e. current domain)
'img-src': ["'self'"],
// Allow CSS loaded from https://fonts.googleapis.com
'style-src': ["'self'", "https://fonts.googleapis.com"],
// Omit `media-src` from policy
// Browser will fallback to default-src for media resources (which is 'none', see above)
'media-src': null
},
reportOnly: false
};
};
```

Expand Down
3 changes: 0 additions & 3 deletions ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

module.exports = function(defaults) {
let app = new EmberAddon(defaults, {
'ember-cli-content-security-policy': {
enabled: false
}
});

/*
Expand Down
13 changes: 6 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
'use strict';
let chalk = require('chalk');

let buildPolicyString = require('./lib/utils')['buildPolicyString'];

const CONFIG_KEY = 'ember-cli-content-security-policy';
let { buildPolicyString, readConfig } = require('./lib/utils');

const CSP_SELF = "'self'";
const CSP_NONE = "'none'";
Expand Down Expand Up @@ -212,15 +210,16 @@ module.exports = {
// these hooks but is private API.
included: function(app) {
let environment = app.env;
let buildConfig = app.options || {}; // ember-cli-build.js
let ownConfig = readConfig(app.project, environment); // config/content-security-policy.js
let buildConfig = app.options || {}; // build-time configuration including livereload and ssl options
let runConfig = app.project.config(); // config/environment.js
let ui = app.project.ui;

this._config = calculateConfig(environment, buildConfig, runConfig, ui);
this._config = calculateConfig(environment, ownConfig, buildConfig, runConfig, ui);
},
};

function calculateConfig(environment, buildConfig, runConfig, ui) {
function calculateConfig(environment, ownConfig, buildConfig, runConfig, ui) {
let config = {
delivery: [DELIVERY_HEADER],
enabled: true,
Expand Down Expand Up @@ -271,7 +270,7 @@ function calculateConfig(environment, buildConfig, runConfig, ui) {
}

// apply configuration
Object.assign(config, buildConfig[CONFIG_KEY]);
Object.assign(config, ownConfig);

return config;
}
Expand Down
38 changes: 34 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/* global module */
/* eslint-env node */

'use strict';

var unique = function(array) {
const fs = require('fs');
const path = require('path');

const unique = function(array) {
return array.filter(function(value, index, self) {
return self.indexOf(value) === index;
});
};

var buildPolicyString = function(policyObject) {
const buildPolicyString = function(policyObject) {
return Object.keys(policyObject).reduce(function(memo, name) {
var value = policyObject[name];
if (value === null) {
Expand All @@ -22,4 +26,30 @@ var buildPolicyString = function(policyObject) {
}, '').trim();
};

module.exports = { buildPolicyString: buildPolicyString };
const getConfigPath = function(projectPkg, projectRoot) {
let configDir = 'config';

if (projectPkg['ember-addon'] && projectPkg['ember-addon']['configPath']) {
configDir = projectPkg['ember-addon']['configPath'];
}

return path.join(projectRoot, configDir, 'content-security-policy.js');
};

/**
* Returns the configuration stored in `config/content-security-policy.js`.
* Returns an empty object if that file does not exist.
*
* @param {string} projectRoot
* @return {object}
*/
const readConfig = function(project, environment) {
let configPath = getConfigPath(project.pkg, project.root);

return fs.existsSync(configPath) ? require(configPath)(environment) : {};
};

module.exports = {
buildPolicyString,
readConfig
};
39 changes: 12 additions & 27 deletions node-tests/e2e/deliver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,11 @@ const fs = require('fs-extra');

const CSP_META_TAG_REG_EXP = /<meta http-equiv="Content-Security-Policy" content="(.*)">/i;

let defaultConfig;
async function setConfig(app, customConfig) {
let configPath = app.filePath('ember-cli-build.js');
async function setConfig(app, config) {
let file = app.filePath('config/content-security-policy.js');
let content = `module.exports = function() { return ${JSON.stringify(config)}; }`;

// store the untouched config generated by ember blueprint
// on first run and reuse for all others to prevent leaking
// between tests
if (!defaultConfig) {
defaultConfig = await fs.readFile(configPath, 'utf8');
}

let config = defaultConfig.replace(/{\s*\/\/ Add options here\s*}/g, JSON.stringify(customConfig));

await fs.writeFile(configPath, config);
await fs.writeFile(file, content);
}

describe('e2e: delivers CSP as configured', function() {
Expand All @@ -39,13 +30,11 @@ describe('e2e: delivers CSP as configured', function() {
describe('', function() {
beforeEach(async function() {
await setConfig(app, {
'ember-cli-content-security-policy': {
delivery: ['header', 'meta'],
policy: {
'font-src': ["'self'", "http://fonts.gstatic.com"],
},
reportOnly: false,
}
delivery: ['header', 'meta'],
policy: {
'font-src': ["'self'", "http://fonts.gstatic.com"],
},
reportOnly: false,
});

await app.startServer({
Expand Down Expand Up @@ -91,10 +80,8 @@ describe('e2e: delivers CSP as configured', function() {

it('uses Content-Security-Policy-Report-Only header if `reportOnly` option is `true`', async function() {
await setConfig(app, {
'ember-cli-content-security-policy': {
delivery: ['header'],
reportOnly: true,
}
delivery: ['header'],
reportOnly: true,
});

await app.startServer();
Expand All @@ -118,9 +105,7 @@ describe('e2e: delivers CSP as configured', function() {

it('does not deliver CSP if `enabled` option is `false`', async function() {
await setConfig(app, {
'ember-cli-content-security-policy': {
enabled: false,
}
enabled: false,
});

await app.startServer();
Expand Down
27 changes: 18 additions & 9 deletions node-tests/unit/config-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,31 @@ describe('unit: configuration', function() {
});

it('is enabled by default', function() {
let config = calculateConfig('development', {}, {}, UIMock);
let config = calculateConfig('development', {}, {}, {}, UIMock);
expect(config.enabled).to.be.true;
});

it('delivers CSP by HTTP header by default', function() {
let config = calculateConfig('development', {}, {}, UIMock);
let config = calculateConfig('development', {}, {}, {}, UIMock);
expect(config.delivery).to.deep.equal(['header']);
});

it('defaults to report only mode', function() {
let config = calculateConfig('development', {}, {}, UIMock);
let config = calculateConfig('development', {}, {}, {}, UIMock);
expect(config.reportOnly).to.be.true;
});

it('replaces default policy object with application config', function() {
let config = calculateConfig(
'development',
{
'ember-cli-content-security-policy': {
policy: {
'default-src': ["'self'"],
'font-src': ['examples.com']
}
policy: {
'default-src': ["'self'"],
'font-src': ['examples.com']
}
},
{},
{},
UIMock
);
expect(config.policy).to.deep.equal({
Expand All @@ -50,6 +49,7 @@ describe('unit: configuration', function() {
let config = calculateConfig(
'development',
{},
{},
{
contentSecurityPolicy: {
'default-src': ["'self'"],
Expand All @@ -70,12 +70,19 @@ describe('unit: configuration', function() {
});

it('supports `contentSecurityPolicyMeta` config option', function() {
let config = calculateConfig('development', {}, { contentSecurityPolicyMeta: true }, UIMock);
let config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyMeta: true },
UIMock
);
expect(config.delivery).to.include('meta');

config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyMeta: false },
UIMock
);
Expand All @@ -86,6 +93,7 @@ describe('unit: configuration', function() {
let config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyHeader: 'Content-Security-Policy-Report-Only' },
UIMock
);
Expand All @@ -94,6 +102,7 @@ describe('unit: configuration', function() {
config = calculateConfig(
'development',
{},
{},
{ contentSecurityPolicyHeader: 'Content-Security-Policy' },
UIMock
);
Expand Down