Merge pull request #4824 from minrk/sign-notebooks

sign notebooks
This commit is contained in:
Brian E. Granger 2014-01-29 16:38:08 -08:00
commit 7c722d4b6c
8 changed files with 127 additions and 36 deletions

View File

@ -207,12 +207,13 @@ class FileNotebookManager(NotebookManager):
model['path'] = path
model['last_modified'] = last_modified
model['created'] = created
if content is True:
if content:
with io.open(os_path, 'r', encoding='utf-8') as f:
try:
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)
model['content'] = nb
return model
@ -236,6 +237,9 @@ class FileNotebookManager(NotebookManager):
# Save the notebook file
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)
if 'name' in nb['metadata']:
nb['metadata']['name'] = u''
try:

View File

@ -20,9 +20,9 @@ Authors:
import os
from IPython.config.configurable import LoggingConfigurable
from IPython.nbformat import current
from IPython.nbformat import current, sign
from IPython.utils import py3compat
from IPython.utils.traitlets import Unicode, TraitError
from IPython.utils.traitlets import Instance, Unicode, TraitError
#-----------------------------------------------------------------------------
# Classes
@ -42,6 +42,30 @@ class NotebookManager(LoggingConfigurable):
filename_ext = Unicode(u'.ipynb')
notary = Instance(sign.NotebookNotary)
def _notary_default(self):
return sign.NotebookNotary(parent=self)
def check_and_sign(self, nb, path, name):
"""Check for trusted cells, and sign the notebook.
Called as a part of saving notebooks.
"""
if self.notary.check_cells(nb):
self.notary.sign(nb)
else:
self.log.warn("Saving untrusted notebook %s/%s", path, name)
def mark_trusted_cells(self, nb, path, name):
"""Mark cells as trusted if the notebook signature matches.
Called as a part of loading notebooks.
"""
trusted = self.notary.check_signature(nb)
if not trusted:
self.log.warn("Notebook %s/%s is not trusted", path, name)
self.notary.mark_cells(nb, trusted)
def path_exists(self, path):
"""Does the API-style path (directory) actually exist?

View File

@ -530,6 +530,7 @@ var IPython = (function (IPython) {
} else {
this.set_input_prompt();
}
this.output_area.trusted = data.trusted || false;
this.output_area.fromJSON(data.outputs);
if (data.collapsed !== undefined) {
if (data.collapsed) {
@ -552,6 +553,7 @@ var IPython = (function (IPython) {
var outputs = this.output_area.toJSON();
data.outputs = outputs;
data.language = 'python';
data.trusted = this.output_area.trusted;
data.collapsed = this.collapsed;
return data;
};

View File

@ -31,6 +31,7 @@ var IPython = (function (IPython) {
this.outputs = [];
this.collapsed = false;
this.scrolled = false;
this.trusted = true;
this.clear_queued = null;
if (prompt_area === undefined) {
this.prompt_area = true;
@ -309,7 +310,7 @@ var IPython = (function (IPython) {
});
return json;
};
OutputArea.prototype.append_output = function (json) {
this.expand();
// Clear the output if clear is queued.
@ -331,6 +332,7 @@ var IPython = (function (IPython) {
} else if (json.output_type === 'stream') {
this.append_stream(json);
}
this.outputs.push(json);
// Only reset the height to automatic if the height is currently
@ -526,12 +528,26 @@ var IPython = (function (IPython) {
'text/plain'
];
OutputArea.safe_outputs = {
'text/plain' : true,
'image/png' : true,
'image/jpeg' : true
};
OutputArea.prototype.append_mime_type = function (json, element) {
for (var type_i in OutputArea.display_order) {
var type = OutputArea.display_order[type_i];
var append = OutputArea.append_map[type];
if ((json[type] !== undefined) && append) {
if (!this.trusted && !OutputArea.safe_outputs[type]) {
// not trusted show warning and do not display
var content = {
text : "Untrusted " + type + " output ignored.",
stream : "stderr"
}
this.append_stream(content);
continue;
}
var md = json.metadata || {};
append.apply(this, [json[type], md, element]);
return true;
@ -753,6 +769,7 @@ var IPython = (function (IPython) {
// clear all, no need for logic
this.element.html("");
this.outputs = [];
this.trusted = true;
this.unscroll_area();
return;
};
@ -765,13 +782,6 @@ var IPython = (function (IPython) {
var len = outputs.length;
var data;
// We don't want to display javascript on load, so remove it from the
// display order for the duration of this function call, but be sure to
// put it back in there so incoming messages that contain javascript
// representations get displayed
var js_index = OutputArea.display_order.indexOf('application/javascript');
OutputArea.display_order.splice(js_index, 1);
for (var i=0; i<len; i++) {
data = outputs[i];
var msg_type = data.output_type;
@ -784,9 +794,6 @@ var IPython = (function (IPython) {
this.append_output(data);
}
// reinsert javascript into display order, see note above
OutputArea.display_order.splice(js_index, 0, 'application/javascript');
};

View File

@ -27,9 +27,9 @@ function assert_has(short_name, json, result, result2) {
this.test.assertTrue(json[0].hasOwnProperty(short_name),
'toJSON() representation uses ' + short_name);
this.test.assertTrue(result.hasOwnProperty(long_name),
'toJSON() original embeded JSON keeps ' + long_name);
'toJSON() original embedded JSON keeps ' + long_name);
this.test.assertTrue(result2.hasOwnProperty(long_name),
'fromJSON() embeded ' + short_name + ' gets mime key ' + long_name);
'fromJSON() embedded ' + short_name + ' gets mime key ' + long_name);
}
// helper function for checkout that the first two cells have a particular
@ -40,11 +40,11 @@ function check_output_area(output_type, keys) {
this.wait_for_output(0);
json = this.evaluate(function() {
var json = IPython.notebook.get_cell(0).output_area.toJSON();
// appended cell will initially be empty, lets add it some output
var cell = IPython.notebook.get_cell(1).output_area.fromJSON(json);
// appended cell will initially be empty, let's add some output
IPython.notebook.get_cell(1).output_area.fromJSON(json);
return json;
});
// The evaluate call above happens asyncrhonously: wait for cell[1] to have output
// The evaluate call above happens asynchronously: wait for cell[1] to have output
this.wait_for_output(1);
var result = this.get_output_cell(0);
var result2 = this.get_output_cell(1);
@ -88,12 +88,19 @@ casper.notebook_test(function () {
var num_cells = this.get_cells_length();
this.test.assertEquals(num_cells, 2, '%%javascript magic works');
this.test.assertTrue(result.hasOwnProperty('application/javascript'),
'testing JS embeded with mime key');
'testing JS embedded with mime key');
});
//this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
this.then(function () {
clear_and_execute(this, [
"%%javascript",
"var a=5;"
].join('\n'));
});
this.then(function ( ) {
this.then(function () {
check_output_area.apply(this, ['display_data', ['javascript']]);
});
@ -223,7 +230,9 @@ casper.notebook_test(function () {
});
this.then(function ( ) {
this.wait_for_output(0, 1);
this.then(function () {
var long_name = 'text/superfancymimetype';
var result = this.get_output_cell(0);
this.test.assertTrue(result.hasOwnProperty(long_name),

View File

@ -35,8 +35,10 @@ casper.notebook_test(function () {
});
this.then(function () {
var result = this.get_output_cell(0);
this.test.assertFalsy(result, "after shutdown: no execution results");
var outputs = this.evaluate(function() {
return IPython.notebook.get_cell(0).output_area.outputs;
})
this.test.assertEquals(outputs.length, 0, "after shutdown: no execution results");
this.test.assertNot(this.kernel_running(),
'after shutdown: IPython.notebook.kernel.running is false ');
});

View File

@ -57,15 +57,18 @@ casper.delete_current_notebook = function () {
});
};
// wait for output in a given cell
casper.wait_for_output = function (cell_num) {
this.waitFor(function (c) {
return this.evaluate(function get_output(c) {
var cell = IPython.notebook.get_cell(c);
return cell.output_area.outputs.length != 0;
},
// pass parameter from the test suite js to the browser code js
{c : cell_num});
// wait for the nth output in a given cell
casper.wait_for_output = function (cell_num, out_num) {
out_num = out_num || 0;
this.then(function() {
this.waitFor(function (c, o) {
return this.evaluate(function get_output(c, o) {
var cell = IPython.notebook.get_cell(c);
return cell.output_area.outputs.length > o;
},
// pass parameter from the test suite js to the browser code js
{c : cell_num, o : out_num});
});
},
function then() { },
function timeout() {
@ -73,7 +76,7 @@ casper.wait_for_output = function (cell_num) {
});
};
// return the output of a given cell
// return an output of a given cell
casper.get_output_cell = function (cell_num, out_num) {
out_num = out_num || 0;
var result = casper.evaluate(function (c, o) {
@ -81,7 +84,18 @@ casper.get_output_cell = function (cell_num, out_num) {
return cell.output_area.outputs[o];
},
{c : cell_num, o : out_num});
return result;
if (!result) {
var num_outputs = casper.evaluate(function (c) {
var cell = IPython.notebook.get_cell(c);
return cell.output_area.outputs.length;
},
{c : cell_num});
this.test.assertTrue(false,
"Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
);
} else {
return result;
}
};
// return the number of cells in the notebook

View File

@ -462,6 +462,35 @@ on available options, use::
:ref:`notebook_public_server`
.. _signing_notebooks:
Signing Notebooks
-----------------
To prevent untrusted code from executing on users' behalf when notebooks open,
we have added a signature to the notebook, stored in metadata.
The notebook server verifies this signature when a notebook is opened.
If the signature stored in the notebook metadata does not match,
javascript and HTML output will not be displayed on load,
and must be regenerated by re-executing the cells.
Any notebook that you have executed yourself *in its entirety* will be considered trusted,
and its HTML and javascript output will be displayed on load.
If you need to see HTML or Javascript output without re-executing,
you can explicitly trust notebooks, such as those shared with you,
or those that you have written yourself prior to IPython 2.0,
at the command-line with::
$ ipython trust mynotebook.ipynb [other notebooks.ipynb]
This just generates a new signature stored in each notebook.
You can generate a new notebook signing key with::
$ ipython trust --reset
Importing ``.py`` files
-----------------------