Skip to content

Commit

Permalink
[FEATURE] server: Expose configuration options for SAP CSP policies
Browse files Browse the repository at this point in the history
  • Loading branch information
RandomByte committed Jul 1, 2021
1 parent 6b00887 commit 55d6a96
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 9 deletions.
19 changes: 16 additions & 3 deletions lib/middleware/MiddlewareManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const middlewareRepository = require("./middlewareRepository");
const MiddlewareUtil = require("./MiddlewareUtil");
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);

/**
*
Expand Down Expand Up @@ -142,13 +143,25 @@ class MiddlewareManager {
}
};
if (this.options.sendSAPTargetCSP) {
Object.assign(oCspConfig, {
const defaultSAPTargetConfig = {
defaultPolicy: "sap-target-level-1",
defaultPolicyIsReportOnly: true,
defaultPolicy2: "sap-target-level-3",
defaultPolicy2: "sap-target-level-2",
defaultPolicy2IsReportOnly: true,
ignorePaths: ["test-resources/sap/ui/qunit/testrunner.html"]
});
};
Object.assign(oCspConfig, defaultSAPTargetConfig);

if (typeof this.options.sendSAPTargetCSP === "object") {
for (const [name, value] of Object.entries(this.options.sendSAPTargetCSP)) {
if (!hasOwn(defaultSAPTargetConfig, name)) {
throw new TypeError(
`Unknown SAP Target CSP configuration option '${name}'. Allowed options are ` +
`${Object.keys(defaultSAPTargetConfig)}`);
}
oCspConfig[name] = value;
}
}
}
if (this.options.serveCSPReports) {
Object.assign(oCspConfig, {
Expand Down
27 changes: 21 additions & 6 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ function _addSsl({app, key, cert}) {
return require("spdy").createServer({cert, key}, app);
}


/**
* SAP target CSP middleware options
*
* @public
* @typedef {object} module:@ui5/server.server.SAPTargetCSPOptions
* @property {string} [defaultPolicy="sap-target-level-1"]
* @property {string} [defaultPolicyIsReportOnly=true]
* @property {string} [defaultPolicy2="sap-target-level-2"]
* @property {string} [defaultPolicy2IsReportOnly=true]
* @property {string[]} [ignorePaths=["test-resources/sap/ui/qunit/testrunner.html"]]
*/


/**
* @public
* @namespace
Expand All @@ -105,14 +119,15 @@ module.exports = {
* @param {boolean} [options.h2=false] Whether HTTP/2 should be used - defaults to <code>http</code>
* @param {string} [options.key] Path to private key to be used for https
* @param {string} [options.cert] Path to certificate to be used for for https
* @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing
* @param {boolean} [options.acceptRemoteConnections=false] If true, listens to remote connections and
* not only to localhost connections
* @param {boolean} [options.sendSAPTargetCSP=false] If true, then the content security policies that SAP and UI5
* aim for (AKA 'target policies'), are send for any requested
* <code>*.html</code> file
* @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing
* @param {boolean} [options.serveCSPReports=false] Enable csp reports serving for request url
* '/.ui5/csp/csp-reports.json'
* @param {boolean|module:@ui5/server.server.SAPTargetCSPOptions} [options.sendSAPTargetCSP=false]
* If set to <code>true</code> or an object, then the default (or configured)
* set of security policies that SAP and UI5 aim for (AKA 'target policies'),
* are send for any requested <code>*.html</code> file
* @param {boolean} [options.serveCSPReports=false] Enable CSP reports serving for request url
* '/.ui5/csp/csp-reports.json'
* @returns {Promise<object>} Promise resolving once the server is listening.
* It resolves with an object containing the <code>port</code>,
* <code>h2</code>-flag and a <code>close</code> function,
Expand Down
167 changes: 167 additions & 0 deletions test/lib/server/middleware/MiddlewareManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,170 @@ test("addCustomMiddleware: wrapperCallback provides middlewareUtil to custom mid
middlewareUtil: "interfacedMiddlewareUtil"
}, "Middleware module got called with correct arguments");
});

test("addStandardMiddleware: CSP middleware configured correctly (default)", async (t) => {
const middlewareManager = new MiddlewareManager({
tree: {},
resources: {
all: "I",
rootProject: "like",
dependencies: "ponies"
}
});
const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves();
await middlewareManager.addStandardMiddleware();

const cspCall = addMiddlewareStub.getCalls().find((call) => {
if (call.args[0] === "csp") {
return true;
}
});
t.truthy(cspCall, "CSP middleware added");
const wrapperCallback = cspCall.args[1].wrapperCallback;

const middlewareModuleStub = sinon.stub().returns("ok");
const middlewareModuleInfo = {
middleware: middlewareModuleStub
};
const middlewareWrapper = wrapperCallback(middlewareModuleInfo);
const res = middlewareWrapper();
t.deepEqual(res, "ok", "Wrapper callback returned expected value");
t.deepEqual(middlewareModuleStub.callCount, 1, "Middleware module got called once");
t.deepEqual(middlewareModuleStub.getCall(0).args[0], "sap-ui-xx-csp-policy",
"CSP middleware module got called with correct first argument");
t.deepEqual(middlewareModuleStub.getCall(0).args[1], {
allowDynamicPolicyDefinition: true,
allowDynamicPolicySelection: true,
definedPolicies: {
/* eslint-disable max-len */
"sap-target-level-1":
`default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-2":
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-3":
`default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' https:; media-src 'self' https:; object-src 'self'; frame-src 'self' https: gap: mailto: tel:; worker-src 'self'; child-src 'self'; connect-src 'self' https: wss:; base-uri 'self';`,
/* eslint-enable max-len */
}
}, "CSP middleware module got called with correct second argument");
});

test("addStandardMiddleware: CSP middleware configured correctly (enabled)", async (t) => {
const middlewareManager = new MiddlewareManager({
tree: {},
resources: {
all: "I",
rootProject: "like",
dependencies: "ponies"
},
options: {
sendSAPTargetCSP: true,
serveCSPReports: true
}
});
const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves();
await middlewareManager.addStandardMiddleware();

const cspCall = addMiddlewareStub.getCalls().find((call) => {
if (call.args[0] === "csp") {
return true;
}
});
t.truthy(cspCall, "CSP middleware added");
const wrapperCallback = cspCall.args[1].wrapperCallback;

const middlewareModuleStub = sinon.stub().returns("ok");
const middlewareModuleInfo = {
middleware: middlewareModuleStub
};
const middlewareWrapper = wrapperCallback(middlewareModuleInfo);
const res = middlewareWrapper();
t.deepEqual(res, "ok", "Wrapper callback returned expected value");
t.deepEqual(middlewareModuleStub.callCount, 1, "Middleware module got called once");
t.deepEqual(middlewareModuleStub.getCall(0).args[0], "sap-ui-xx-csp-policy",
"CSP middleware module got called with correct first argument");
t.deepEqual(middlewareModuleStub.getCall(0).args[1], {
allowDynamicPolicyDefinition: true,
allowDynamicPolicySelection: true,
definedPolicies: {
/* eslint-disable max-len */
"sap-target-level-1":
`default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-2":
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-3":
`default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' https:; media-src 'self' https:; object-src 'self'; frame-src 'self' https: gap: mailto: tel:; worker-src 'self'; child-src 'self'; connect-src 'self' https: wss:; base-uri 'self';`,
/* eslint-enable max-len */
},
defaultPolicy: "sap-target-level-1",
defaultPolicyIsReportOnly: true,
defaultPolicy2: "sap-target-level-2",
defaultPolicy2IsReportOnly: true,
ignorePaths: [
"test-resources/sap/ui/qunit/testrunner.html",
],
serveCSPReports: true
}, "CSP middleware module got called with correct second argument");
});

test("addStandardMiddleware: CSP middleware configured correctly (custom)", async (t) => {
const middlewareManager = new MiddlewareManager({
tree: {},
resources: {
all: "I",
rootProject: "like",
dependencies: "ponies"
},
options: {
sendSAPTargetCSP: {
defaultPolicy: "sap-target-level-1",
defaultPolicyIsReportOnly: false,
defaultPolicy2: "sap-target-level-3",
defaultPolicy2IsReportOnly: true,
ignorePaths: ["lord/tirek.html"]
},
serveCSPReports: false
}
});
const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves();
await middlewareManager.addStandardMiddleware();

const cspCall = addMiddlewareStub.getCalls().find((call) => {
if (call.args[0] === "csp") {
return true;
}
});
t.truthy(cspCall, "CSP middleware added");
const wrapperCallback = cspCall.args[1].wrapperCallback;

const middlewareModuleStub = sinon.stub().returns("ok");
const middlewareModuleInfo = {
middleware: middlewareModuleStub
};
const middlewareWrapper = wrapperCallback(middlewareModuleInfo);
const res = middlewareWrapper();
t.deepEqual(res, "ok", "Wrapper callback returned expected value");
t.deepEqual(middlewareModuleStub.callCount, 1, "Middleware module got called once");
t.deepEqual(middlewareModuleStub.getCall(0).args[0], "sap-ui-xx-csp-policy",
"CSP middleware module got called with correct first argument");
t.deepEqual(middlewareModuleStub.getCall(0).args[1], {
allowDynamicPolicyDefinition: true,
allowDynamicPolicySelection: true,
definedPolicies: {
/* eslint-disable max-len */
"sap-target-level-1":
`default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-2":
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' https: http: data: blob:; media-src 'self' https: http: data: blob:; object-src blob:; frame-src 'self' https: gap: data: blob: mailto: tel:; worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' https: wss:; base-uri 'self';`,
"sap-target-level-3":
`default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' https:; media-src 'self' https:; object-src 'self'; frame-src 'self' https: gap: mailto: tel:; worker-src 'self'; child-src 'self'; connect-src 'self' https: wss:; base-uri 'self';`,
/* eslint-enable max-len */
},
defaultPolicy: "sap-target-level-1",
defaultPolicyIsReportOnly: false,
defaultPolicy2: "sap-target-level-3",
defaultPolicy2IsReportOnly: true,
ignorePaths: [
"lord/tirek.html",
]
}, "CSP middleware module got called with correct second argument");
});

0 comments on commit 55d6a96

Please sign in to comment.