[\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