diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index 4187249ad..acd3997ba 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -275,7 +275,7 @@ class FileNotebookManager(NotebookManager): nb = current.read(f, u'json') except Exception as e: raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) - self.mark_trusted_cells(nb, path, name) + self.mark_trusted_cells(nb, name, path) model['content'] = nb return model @@ -300,7 +300,7 @@ class FileNotebookManager(NotebookManager): os_path = self._get_os_path(new_name, new_path) nb = current.to_notebook_json(model['content']) - self.check_and_sign(nb, new_path, new_name) + self.check_and_sign(nb, new_name, new_path) if 'name' in nb['metadata']: nb['metadata']['name'] = u'' diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index de62bd93b..9c263c62e 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -222,6 +222,17 @@ class NotebookHandler(IPythonHandler): self.finish() +class NotebookTrustHandler(IPythonHandler): + + SUPPORTED_METHODS = ('POST') + + @web.authenticated + def post(self, path='', name=None): + """trust the specified notebook""" + self.notebook_manager.trust_notebook(name, path) + self.set_status(200) + + class NotebookCheckpointsHandler(IPythonHandler): SUPPORTED_METHODS = ('GET', 'POST') @@ -279,6 +290,7 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): _checkpoint_id_regex = r"(?P[\w-]+)" default_handlers = [ + (r"/api/notebooks%s/trust" % notebook_path_regex, NotebookTrustHandler), (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler), (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex), ModifyNotebookCheckpointsHandler), @@ -286,5 +298,3 @@ default_handlers = [ (r"/api/notebooks%s" % path_regex, NotebookHandler), ] - - diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index f3fb3fc8d..b578dc53e 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -224,9 +224,18 @@ class NotebookManager(LoggingConfigurable): def log_info(self): self.log.info(self.info_string()) - # NotebookManager methods provided for use in subclasses. - - def check_and_sign(self, nb, path, name): + def trust_notebook(self, name, path=''): + """Check for trusted cells, and sign the notebook. + + Called as a part of saving notebooks. + """ + model = self.get_notebook(name, path) + nb = model['content'] + self.log.warn("Trusting notebook %s/%s", path, name) + self.notary.mark_cells(nb, True) + self.save_notebook(model, name, path) + + def check_and_sign(self, nb, name, path=''): """Check for trusted cells, and sign the notebook. Called as a part of saving notebooks. @@ -236,7 +245,7 @@ class NotebookManager(LoggingConfigurable): else: self.log.warn("Saving untrusted notebook %s/%s", path, name) - def mark_trusted_cells(self, nb, path, name): + def mark_trusted_cells(self, nb, name, path=''): """Mark cells as trusted if the notebook signature matches. Called as a part of loading notebooks. diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index fa37169d4..7c3eb4ae8 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -2,12 +2,15 @@ """Tests for the notebook manager.""" from __future__ import print_function +import logging import os from tornado.web import HTTPError from unittest import TestCase from tempfile import NamedTemporaryFile +from IPython.nbformat import current + from IPython.utils.tempdir import TemporaryDirectory from IPython.utils.traitlets import TraitError from IPython.html.utils import url_path_join @@ -55,6 +58,14 @@ class TestFileNotebookManager(TestCase): class TestNotebookManager(TestCase): + def setUp(self): + self._temp_dir = TemporaryDirectory() + self.td = self._temp_dir.name + self.nbm = FileNotebookManager(notebook_dir=self.td, log=logging.getLogger()) + + def tearDown(self): + self._temp_dir.cleanup() + def make_dir(self, abs_path, rel_path): """make subdirectory, rel_path is the relative path to that directory from the location where the server started""" @@ -63,11 +74,33 @@ class TestNotebookManager(TestCase): os.makedirs(os_path) except OSError: print("Directory already exists: %r" % os_path) - + + def add_code_cell(self, nb): + output = current.new_output("display_data", output_javascript="alert('hi');") + cell = current.new_code_cell("print('hi')", outputs=[output]) + if not nb.worksheets: + nb.worksheets.append(current.new_worksheet()) + nb.worksheets[0].cells.append(cell) + + def new_notebook(self): + nbm = self.nbm + model = nbm.create_notebook() + name = model['name'] + path = model['path'] + + full_model = nbm.get_notebook(name, path) + nb = full_model['content'] + self.add_code_cell(nb) + + nbm.save_notebook(full_model, name, path) + return nb, name, path + def test_create_notebook(self): with TemporaryDirectory() as td: # Test in root directory + # Create a notebook nm = FileNotebookManager(notebook_dir=td) + # Test in root directory model = nm.create_notebook() assert isinstance(model, dict) self.assertIn('name', model) @@ -243,3 +276,43 @@ class TestNotebookManager(TestCase): copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) self.assertEqual(copy2['name'], u'copy 2.ipynb') + def test_trust_notebook(self): + nbm = self.nbm + nb, name, path = self.new_notebook() + + untrusted = nbm.get_notebook(name, path)['content'] + assert not nbm.notary.check_signature(untrusted) + + nbm.trust_notebook(name, path) + trusted = nbm.get_notebook(name, path)['content'] + assert nbm.notary.check_signature(trusted) + + def test_mark_trusted_cells(self): + nbm = self.nbm + nb, name, path = self.new_notebook() + + nbm.mark_trusted_cells(nb, name, path) + for cell in nb.worksheets[0].cells: + if cell.cell_type == 'code': + assert not cell.trusted + + nbm.trust_notebook(name, path) + nb = nbm.get_notebook(name, path)['content'] + nbm.mark_trusted_cells(nb, name, path) + for cell in nb.worksheets[0].cells: + if cell.cell_type == 'code': + assert cell.trusted + + def test_check_and_sign(self): + nbm = self.nbm + nb, name, path = self.new_notebook() + + nbm.mark_trusted_cells(nb, name, path) + nbm.check_and_sign(nb, name, path) + assert not nbm.notary.check_signature(nb) + + nbm.trust_notebook(name, path) + nb = nbm.get_notebook(name, path)['content'] + nbm.mark_trusted_cells(nb, name, path) + nbm.check_and_sign(nb, name, path) + assert nbm.notary.check_signature(nb) diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index dacf18e24..747ae2ed5 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -133,6 +133,9 @@ var IPython = (function (IPython) { }); this.element.find('#restore_checkpoint').click(function () { }); + this.element.find('#trust_notebook').click(function () { + IPython.notebook.trust_notebook(); + }); this.element.find('#kill_and_exit').click(function () { IPython.notebook.session.delete(); setTimeout(function(){ diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index d49fd4984..15399951e 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -1786,6 +1786,75 @@ var IPython = (function (IPython) { $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]); }; + /** + * Explicitly trust the output of this notebook. + * + * @method trust_notebook + */ + Notebook.prototype.trust_notebook = function (extra_settings) { + // We do the call with settings so we can set cache to false. + + var settings = { + processData : false, + cache : false, + type : "POST", + success : $.proxy(this._trust_notebook_success, this), + error : $.proxy(this._trust_notebook_error, this) + }; + if (extra_settings) { + for (var key in extra_settings) { + settings[key] = extra_settings[key]; + } + } + + var body = $("
").append($("

") + .text("A trusted IPython notebook may execute hidden malicious code ") + .append($("") + .append( + $("").text("when you open it") + ) + ).append(".") + ).append($("

") + .text("For more information, see the ") + .append($("").attr("href", "http://ipython.org/security.html") + .text("IPython security documentation") + ).append(".") + ); + + var nb = this; + IPython.dialog.modal({ + title: "Trust this notebook?", + body: body, + + buttons: { + Cancel : {}, + Trust : { + class : "btn-danger", + click : function () { + $([IPython.events]).trigger('notebook_trusting.Notebook'); + var url = utils.url_join_encode( + nb.base_url, + 'api/notebooks', + nb.notebook_path, + nb.notebook_name, + 'trust' + ); + $.ajax(url, settings); + } + } + } + }); + }; + + Notebook.prototype._trust_notebook_success = function (data, status, xhr) { + $([IPython.events]).trigger('notebook_trusted.Notebook'); + window.location.reload(); + }; + + Notebook.prototype._trust_notebook_error = function (xhr, status, error) { + $([IPython.events]).trigger('notebook_trust_failed.Notebook', [xhr, status, error]); + }; + Notebook.prototype.new_notebook = function(){ var path = this.notebook_path; var base_url = this.base_url; diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 2252496ad..c1adc2165 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -86,7 +86,10 @@ class="notebook_app"

  • - +
  • + Trust Notebook
  • +
  • Close and halt