diff --git a/IPython/config/application.py b/IPython/config/application.py index 298b0fc..69a317f 100644 --- a/IPython/config/application.py +++ b/IPython/config/application.py @@ -6,6 +6,7 @@ from __future__ import print_function +import json import logging import os import re @@ -535,8 +536,17 @@ class Application(SingletonConfigurable): def load_config_file(self, filename, path=None): """Load config files by filename and path.""" filename, ext = os.path.splitext(filename) + loaded = [] for config in self._load_config_files(filename, path=path, log=self.log): + loaded.append(config) self.update_config(config) + if len(loaded) > 1: + collisions = loaded[0].collisions(loaded[1]) + if collisions: + self.log.warn("Collisions detected in {0}.py and {0}.json config files." + " {0}.json has higher priority: {1}".format( + filename, json.dumps(collisions, indent=2), + )) def generate_config_file(self): diff --git a/IPython/config/loader.py b/IPython/config/loader.py index 160739b..b731b8d 100644 --- a/IPython/config/loader.py +++ b/IPython/config/loader.py @@ -193,7 +193,27 @@ class Config(dict): to_update[k] = copy.deepcopy(v) self.update(to_update) - + + def collisions(self, other): + """Check for collisions between two config objects. + + Returns a dict of the form {"Class": {"trait": "collision message"}}`, + indicating which values have been ignored. + + An empty dict indicates no collisions. + """ + collisions = {} + for section in self: + if section not in other: + continue + mine = self[section] + theirs = other[section] + for key in mine: + if key in theirs and mine[key] != theirs[key]: + collisions.setdefault(section, {}) + collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key]) + return collisions + def __contains__(self, key): # allow nested contains of the form `"Section.key" in config` if '.' in key: @@ -565,7 +585,7 @@ class KeyValueConfigLoader(CommandLineConfigLoader): def _decode_argv(self, argv, enc=None): - """decode argv if bytes, using stin.encoding, falling back on default enc""" + """decode argv if bytes, using stdin.encoding, falling back on default enc""" uargv = [] if enc is None: enc = DEFAULT_ENCODING diff --git a/IPython/config/tests/test_loader.py b/IPython/config/tests/test_loader.py index 0238285..a5fc91d 100644 --- a/IPython/config/tests/test_loader.py +++ b/IPython/config/tests/test_loader.py @@ -1,28 +1,12 @@ # encoding: utf-8 -""" -Tests for IPython.config.loader - -Authors: - -* Brian Granger -* Fernando Perez (design help) -""" +"""Tests for IPython.config.loader""" -#----------------------------------------------------------------------------- -# Copyright (C) 2008 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import os import pickle import sys -import json from tempfile import mkstemp from unittest import TestCase @@ -43,10 +27,6 @@ from IPython.config.loader import ( ConfigError, ) -#----------------------------------------------------------------------------- -# Actual tests -#----------------------------------------------------------------------------- - pyfile = """ c = get_config() @@ -117,6 +97,34 @@ class TestFileCL(TestCase): cl = JSONFileConfigLoader(fname, log=log) config = cl.load_config() self._check_conf(config) + + def test_collision(self): + a = Config() + b = Config() + self.assertEqual(a.collisions(b), {}) + a.A.trait1 = 1 + b.A.trait2 = 2 + self.assertEqual(a.collisions(b), {}) + b.A.trait1 = 1 + self.assertEqual(a.collisions(b), {}) + b.A.trait1 = 0 + self.assertEqual(a.collisions(b), { + 'A': { + 'trait1': "1 ignored, using 0", + } + }) + self.assertEqual(b.collisions(a), { + 'A': { + 'trait1': "0 ignored, using 1", + } + }) + a.A.trait2 = 3 + self.assertEqual(b.collisions(a), { + 'A': { + 'trait1': "0 ignored, using 1", + 'trait2': "2 ignored, using 3", + } + }) def test_v2raise(self): fd, fname = mkstemp('.json') diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index ae58e7a..fa58c2b 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -32,6 +32,8 @@ from IPython.utils.path import filefind from IPython.utils.py3compat import string_types from IPython.html.utils import is_hidden, url_path_join, url_escape +from IPython.html.services.security import csp_report_uri + #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -45,17 +47,22 @@ class AuthenticatedHandler(web.RequestHandler): def set_default_headers(self): headers = self.settings.get('headers', {}) - if "X-Frame-Options" not in headers: - headers["X-Frame-Options"] = "SAMEORIGIN" + if "Content-Security-Policy" not in headers: + headers["Content-Security-Policy"] = ( + "frame-ancestors 'self'; " + # Make sure the report-uri is relative to the base_url + "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";" + ) + # Allow for overriding headers for header_name,value in headers.items() : try: self.set_header(header_name, value) - except Exception: + except Exception as e: # tornado raise Exception (not a subclass) # if method is unsupported (websocket and Access-Control-Allow-Origin # for example, so just ignore) - pass + self.log.debug(e) def clear_login_cookie(self): self.clear_cookie(self.cookie_name) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index e21db1a..24d18e8 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -225,7 +225,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('services.sessions.handlers')) handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) - + handlers.extend(load_handlers('services.security.handlers')) handlers.append( (r"/nbextensions/(.*)", FileFindHandler, { 'path': settings['nbextensions_path'], diff --git a/IPython/html/services/config/__init__.py b/IPython/html/services/config/__init__.py index e69de29..d8d9380 100644 --- a/IPython/html/services/config/__init__.py +++ b/IPython/html/services/config/__init__.py @@ -0,0 +1 @@ +from .manager import ConfigManager diff --git a/IPython/html/services/kernels/tests/test_kernels_api.py b/IPython/html/services/kernels/tests/test_kernels_api.py index 8f29a07..b33142c 100644 --- a/IPython/html/services/kernels/tests/test_kernels_api.py +++ b/IPython/html/services/kernels/tests/test_kernels_api.py @@ -65,7 +65,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) def test_main_kernel_handler(self): # POST request @@ -75,7 +78,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) # GET request r = self.kern_api.list() diff --git a/IPython/html/services/security/__init__.py b/IPython/html/services/security/__init__.py new file mode 100644 index 0000000..9cf0d47 --- /dev/null +++ b/IPython/html/services/security/__init__.py @@ -0,0 +1,4 @@ +# URI for the CSP Report. Included here to prevent a cyclic dependency. +# csp_report_uri is needed both by the BaseHandler (for setting the report-uri) +# and by the CSPReportHandler (which depends on the BaseHandler). +csp_report_uri = r"/api/security/csp-report" diff --git a/IPython/html/services/security/handlers.py b/IPython/html/services/security/handlers.py new file mode 100644 index 0000000..18f7874 --- /dev/null +++ b/IPython/html/services/security/handlers.py @@ -0,0 +1,23 @@ +"""Tornado handlers for security logging.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import gen, web + +from ...base.handlers import IPythonHandler, json_errors +from . import csp_report_uri + +class CSPReportHandler(IPythonHandler): + '''Accepts a content security policy violation report''' + @web.authenticated + @json_errors + def post(self): + '''Log a content security policy violation report''' + csp_report = self.get_json_body() + self.log.warn("Content security violation: %s", + self.request.body.decode('utf8', 'replace')) + +default_handlers = [ + (csp_report_uri, CSPReportHandler) +] diff --git a/IPython/html/static/services/kernels/kernel.js b/IPython/html/static/services/kernels/kernel.js index 28b77ca..dba1315 100644 --- a/IPython/html/static/services/kernels/kernel.js +++ b/IPython/html/static/services/kernels/kernel.js @@ -286,16 +286,16 @@ define([ }); }; - /** - * POST /api/kernels/[:kernel_id]/restart - * - * Restart the kernel. - * - * @function interrupt - * @param {function} [success] - function executed on ajax success - * @param {function} [error] - functon executed on ajax error - */ Kernel.prototype.restart = function (success, error) { + /** + * POST /api/kernels/[:kernel_id]/restart + * + * Restart the kernel. + * + * @function interrupt + * @param {function} [success] - function executed on ajax success + * @param {function} [error] - functon executed on ajax error + */ this.events.trigger('kernel_restarting.Kernel', {kernel: this}); this.stop_channels(); @@ -327,14 +327,14 @@ define([ }); }; - /** - * Reconnect to a disconnected kernel. This is not actually a - * standard HTTP request, but useful function nonetheless for - * reconnecting to the kernel if the connection is somehow lost. - * - * @function reconnect - */ Kernel.prototype.reconnect = function () { + /** + * Reconnect to a disconnected kernel. This is not actually a + * standard HTTP request, but useful function nonetheless for + * reconnecting to the kernel if the connection is somehow lost. + * + * @function reconnect + */ if (this.is_connected()) { return; } @@ -346,15 +346,15 @@ define([ this.start_channels(); }; - /** - * Handle a successful AJAX request by updating the kernel id and - * name from the response, and then optionally calling a provided - * callback. - * - * @function _on_success - * @param {function} success - callback - */ Kernel.prototype._on_success = function (success) { + /** + * Handle a successful AJAX request by updating the kernel id and + * name from the response, and then optionally calling a provided + * callback. + * + * @function _on_success + * @param {function} success - callback + */ var that = this; return function (data, status, xhr) { if (data) { @@ -368,14 +368,14 @@ define([ }; }; - /** - * Handle a failed AJAX request by logging the error message, and - * then optionally calling a provided callback. - * - * @function _on_error - * @param {function} error - callback - */ Kernel.prototype._on_error = function (error) { + /** + * Handle a failed AJAX request by logging the error message, and + * then optionally calling a provided callback. + * + * @function _on_error + * @param {function} error - callback + */ return function (xhr, status, err) { utils.log_ajax_error(xhr, status, err); if (error) { @@ -384,27 +384,27 @@ define([ }; }; - /** - * Perform necessary tasks once the kernel has been started, - * including actually connecting to the kernel. - * - * @function _kernel_created - * @param {Object} data - information about the kernel including id - */ Kernel.prototype._kernel_created = function (data) { + /** + * Perform necessary tasks once the kernel has been started, + * including actually connecting to the kernel. + * + * @function _kernel_created + * @param {Object} data - information about the kernel including id + */ this.id = data.id; this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id); this.start_channels(); }; - /** - * Perform necessary tasks once the connection to the kernel has - * been established. This includes requesting information about - * the kernel. - * - * @function _kernel_connected - */ Kernel.prototype._kernel_connected = function () { + /** + * Perform necessary tasks once the connection to the kernel has + * been established. This includes requesting information about + * the kernel. + * + * @function _kernel_connected + */ this.events.trigger('kernel_connected.Kernel', {kernel: this}); this.events.trigger('kernel_starting.Kernel', {kernel: this}); // get kernel info so we know what state the kernel is in @@ -415,24 +415,24 @@ define([ }); }; - /** - * Perform necessary tasks after the kernel has died. This closing - * communication channels to the kernel if they are still somehow - * open. - * - * @function _kernel_dead - */ Kernel.prototype._kernel_dead = function () { + /** + * Perform necessary tasks after the kernel has died. This closing + * communication channels to the kernel if they are still somehow + * open. + * + * @function _kernel_dead + */ this.stop_channels(); }; - /** - * Start the `shell`and `iopub` channels. - * Will stop and restart them if they already exist. - * - * @function start_channels - */ Kernel.prototype.start_channels = function () { + /** + * Start the `shell`and `iopub` channels. + * Will stop and restart them if they already exist. + * + * @function start_channels + */ var that = this; this.stop_channels(); var ws_host_url = this.ws_url + this.kernel_url; @@ -506,29 +506,29 @@ define([ this.channels.stdin.onmessage = $.proxy(this._handle_input_request, this); }; - /** - * Handle a websocket entering the open state, - * signaling that the kernel is connected when all channels are open. - * - * @function _ws_opened - */ Kernel.prototype._ws_opened = function (evt) { + /** + * Handle a websocket entering the open state, + * signaling that the kernel is connected when all channels are open. + * + * @function _ws_opened + */ if (this.is_connected()) { // all events ready, trigger started event. this._kernel_connected(); } }; - /** - * Handle a websocket entering the closed state. This closes the - * other communication channels if they are open. If the websocket - * was not closed due to an error, try to reconnect to the kernel. - * - * @function _ws_closed - * @param {string} ws_url - the websocket url - * @param {bool} error - whether the connection was closed due to an error - */ Kernel.prototype._ws_closed = function(ws_url, error) { + /** + * Handle a websocket entering the closed state. This closes the + * other communication channels if they are open. If the websocket + * was not closed due to an error, try to reconnect to the kernel. + * + * @function _ws_closed + * @param {string} ws_url - the websocket url + * @param {bool} error - whether the connection was closed due to an error + */ this.stop_channels(); this.events.trigger('kernel_disconnected.Kernel', {kernel: this}); @@ -555,13 +555,13 @@ define([ } }; - /** - * Close the websocket channels. After successful close, the value - * in `this.channels[channel_name]` will be null. - * - * @function stop_channels - */ Kernel.prototype.stop_channels = function () { + /** + * Close the websocket channels. After successful close, the value + * in `this.channels[channel_name]` will be null. + * + * @function stop_channels + */ var that = this; var close = function (c) { return function () { @@ -582,15 +582,15 @@ define([ } }; - /** - * Check whether there is a connection to the kernel. This - * function only returns true if all channel objects have been - * created and have a state of WebSocket.OPEN. - * - * @function is_connected - * @returns {bool} - whether there is a connection - */ Kernel.prototype.is_connected = function () { + /** + * Check whether there is a connection to the kernel. This + * function only returns true if all channel objects have been + * created and have a state of WebSocket.OPEN. + * + * @function is_connected + * @returns {bool} - whether there is a connection + */ for (var c in this.channels) { // if any channel is not ready, then we're not connected if (this.channels[c] === null) { @@ -603,15 +603,15 @@ define([ return true; }; - /** - * Check whether the connection to the kernel has been completely - * severed. This function only returns true if all channel objects - * are null. - * - * @function is_fully_disconnected - * @returns {bool} - whether the kernel is fully disconnected - */ Kernel.prototype.is_fully_disconnected = function () { + /** + * Check whether the connection to the kernel has been completely + * severed. This function only returns true if all channel objects + * are null. + * + * @function is_fully_disconnected + * @returns {bool} - whether the kernel is fully disconnected + */ for (var c in this.channels) { if (this.channels[c] === null) { return true; @@ -620,12 +620,12 @@ define([ return false; }; - /** - * Send a message on the Kernel's shell channel - * - * @function send_shell_message - */ Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) { + /** + * Send a message on the Kernel's shell channel + * + * @function send_shell_message + */ if (!this.is_connected()) { throw new Error("kernel is not connected"); } @@ -635,17 +635,17 @@ define([ return msg.header.msg_id; }; - /** - * Get kernel info - * - * @function kernel_info - * @param callback {function} - * - * When calling this method, pass a callback function that expects one argument. - * The callback will be passed the complete `kernel_info_reply` message documented - * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info) - */ Kernel.prototype.kernel_info = function (callback) { + /** + * Get kernel info + * + * @function kernel_info + * @param callback {function} + * + * When calling this method, pass a callback function that expects one argument. + * The callback will be passed the complete `kernel_info_reply` message documented + * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info) + */ var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; @@ -653,19 +653,19 @@ define([ return this.send_shell_message("kernel_info_request", {}, callbacks); }; - /** - * Get info on an object - * - * When calling this method, pass a callback function that expects one argument. - * The callback will be passed the complete `inspect_reply` message documented - * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information) - * - * @function inspect - * @param code {string} - * @param cursor_pos {integer} - * @param callback {function} - */ Kernel.prototype.inspect = function (code, cursor_pos, callback) { + /** + * Get info on an object + * + * When calling this method, pass a callback function that expects one argument. + * The callback will be passed the complete `inspect_reply` message documented + * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information) + * + * @function inspect + * @param code {string} + * @param cursor_pos {integer} + * @param callback {function} + */ var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; @@ -679,56 +679,56 @@ define([ return this.send_shell_message("inspect_request", content, callbacks); }; - /** - * Execute given code into kernel, and pass result to callback. - * - * @async - * @function execute - * @param {string} code - * @param [callbacks] {Object} With the following keys (all optional) - * @param callbacks.shell.reply {function} - * @param callbacks.shell.payload.[payload_name] {function} - * @param callbacks.iopub.output {function} - * @param callbacks.iopub.clear_output {function} - * @param callbacks.input {function} - * @param {object} [options] - * @param [options.silent=false] {Boolean} - * @param [options.user_expressions=empty_dict] {Dict} - * @param [options.allow_stdin=false] {Boolean} true|false - * - * @example - * - * The options object should contain the options for the execute - * call. Its default values are: - * - * options = { - * silent : true, - * user_expressions : {}, - * allow_stdin : false - * } - * - * When calling this method pass a callbacks structure of the - * form: - * - * callbacks = { - * shell : { - * reply : execute_reply_callback, - * payload : { - * set_next_input : set_next_input_callback, - * } - * }, - * iopub : { - * output : output_callback, - * clear_output : clear_output_callback, - * }, - * input : raw_input_callback - * } - * - * Each callback will be passed the entire message as a single - * arugment. Payload handlers will be passed the corresponding - * payload and the execute_reply message. - */ Kernel.prototype.execute = function (code, callbacks, options) { + /** + * Execute given code into kernel, and pass result to callback. + * + * @async + * @function execute + * @param {string} code + * @param [callbacks] {Object} With the following keys (all optional) + * @param callbacks.shell.reply {function} + * @param callbacks.shell.payload.[payload_name] {function} + * @param callbacks.iopub.output {function} + * @param callbacks.iopub.clear_output {function} + * @param callbacks.input {function} + * @param {object} [options] + * @param [options.silent=false] {Boolean} + * @param [options.user_expressions=empty_dict] {Dict} + * @param [options.allow_stdin=false] {Boolean} true|false + * + * @example + * + * The options object should contain the options for the execute + * call. Its default values are: + * + * options = { + * silent : true, + * user_expressions : {}, + * allow_stdin : false + * } + * + * When calling this method pass a callbacks structure of the + * form: + * + * callbacks = { + * shell : { + * reply : execute_reply_callback, + * payload : { + * set_next_input : set_next_input_callback, + * } + * }, + * iopub : { + * output : output_callback, + * clear_output : clear_output_callback, + * }, + * input : raw_input_callback + * } + * + * Each callback will be passed the entire message as a single + * arugment. Payload handlers will be passed the corresponding + * payload and the execute_reply message. + */ var content = { code : code, silent : true, diff --git a/IPython/html/static/widgets/js/manager.js b/IPython/html/static/widgets/js/manager.js index e7ee686..c0f17fb 100644 --- a/IPython/html/static/widgets/js/manager.js +++ b/IPython/html/static/widgets/js/manager.js @@ -101,8 +101,7 @@ define([ var parameters = {model: model, options: options}; var view = new ViewType(parameters); view.listenTo(model, 'destroy', view.remove); - view.render(); - return view; + return Promise.resolve(view.render()).then(function() {return view;}); }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true)); }); return model.state_change; diff --git a/docs/source/whatsnew/development.rst b/docs/source/whatsnew/development.rst index b003dfa..87b41c5 100644 --- a/docs/source/whatsnew/development.rst +++ b/docs/source/whatsnew/development.rst @@ -180,16 +180,42 @@ Backwards incompatible changes .. DO NOT EDIT THIS LINE BEFORE RELEASE. INCOMPAT INSERTION POINT. -IFrame embedding -```````````````` +Content Security Policy +``````````````````````` -The IPython Notebook and its APIs by default will only be allowed to be -embedded in an iframe on the same origin. +The Content Security Policy is a web standard for adding a layer of security to +detect and mitigate certain classes of attacks, including Cross Site Scripting +(XSS) and data injection attacks. This was introduced into the notebook to +ensure that the IPython Notebook and its APIs (by default) can only be embedded +in an iframe on the same origin. -To override this, set ``headers[X-Frame-Options]`` to one of +Override ``headers['Content-Security-Policy']`` within your notebook +configuration to extend for alternate domains and security settings.:: -* DENY -* SAMEORIGIN -* ALLOW-FROM uri + c.NotebookApp.tornado_settings = { + 'headers': { + 'Content-Security-Policy': "frame-ancestors 'self'" + } + } -See `Mozilla's guide to X-Frame-Options `_ for more examples. +Example policies:: + + Content-Security-Policy: default-src 'self' https://*.jupyter.org + +Matches embeddings on any subdomain of jupyter.org, so long as they are served +over SSL. + +There is a `report-uri `_ endpoint available for logging CSP violations, located at +``/api/security/csp-report``. To use it, set ``report-uri`` as part of the CSP:: + + c.NotebookApp.tornado_settings = { + 'headers': { + 'Content-Security-Policy': "frame-ancestors 'self'; report-uri /api/security/csp-report" + } + } + +It simply provides the CSP report as a warning in IPython's logs. The default +CSP sets this report-uri relative to the ``base_url`` (not shown above). + +For a more thorough and accurate guide on Content Security Policies, check out +`MDN's Using Content Security Policy `_ for more examples.