commit
b60d1542b2
@ -0,0 +1,78 @@
|
||||
// Test the notebook dual mode feature.
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var b = 'print("b")';
|
||||
index = this.append_cell(b);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var c = 'print("c")';
|
||||
index = this.append_cell(c);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
this.validate_notebook_state('initial state', 'edit', 0);
|
||||
this.trigger_keydown('esc');
|
||||
this.validate_notebook_state('esc', 'command', 0);
|
||||
this.trigger_keydown('down');
|
||||
this.validate_notebook_state('down', 'command', 1);
|
||||
this.trigger_keydown('enter');
|
||||
this.validate_notebook_state('enter', 'edit', 1);
|
||||
this.trigger_keydown('j');
|
||||
this.validate_notebook_state('j in edit mode', 'edit', 1);
|
||||
this.trigger_keydown('esc');
|
||||
this.validate_notebook_state('esc', 'command', 1);
|
||||
this.trigger_keydown('j');
|
||||
this.validate_notebook_state('j in command mode', 'command', 2);
|
||||
this.click_cell_editor(0);
|
||||
this.validate_notebook_state('click cell 0', 'edit', 0);
|
||||
this.click_cell_editor(3);
|
||||
this.validate_notebook_state('click cell 3', 'edit', 3);
|
||||
this.trigger_keydown('esc');
|
||||
this.validate_notebook_state('esc', 'command', 3);
|
||||
|
||||
// Open keyboard help
|
||||
this.evaluate(function(){
|
||||
$('#keyboard_shortcuts a').click();
|
||||
}, {});
|
||||
|
||||
this.trigger_keydown('k');
|
||||
this.validate_notebook_state('k in command mode while keyboard help is up', 'command', 3);
|
||||
|
||||
// Close keyboard help
|
||||
this.evaluate(function(){
|
||||
$('div.modal button.close').click();
|
||||
}, {});
|
||||
|
||||
this.trigger_keydown('k');
|
||||
this.validate_notebook_state('k in command mode', 'command', 2);
|
||||
this.click_cell_editor(0);
|
||||
this.validate_notebook_state('click cell 0', 'edit', 0);
|
||||
this.focus_notebook();
|
||||
this.validate_notebook_state('focus #notebook', 'command', 0);
|
||||
this.click_cell_editor(0);
|
||||
this.validate_notebook_state('click cell 0', 'edit', 0);
|
||||
this.focus_notebook();
|
||||
this.validate_notebook_state('focus #notebook', 'command', 0);
|
||||
this.click_cell_editor(3);
|
||||
this.validate_notebook_state('click cell 3', 'edit', 3);
|
||||
|
||||
// Cell deletion
|
||||
this.trigger_keydown('esc', 'd', 'd');
|
||||
this.test.assertEquals(this.get_cells_length(), 3, 'dd actually deletes a cell');
|
||||
this.validate_notebook_state('dd', 'command', 2);
|
||||
|
||||
// Make sure that if the time between d presses is too long, nothing gets removed.
|
||||
this.trigger_keydown('d');
|
||||
});
|
||||
this.wait(1000);
|
||||
this.then(function () {
|
||||
this.trigger_keydown('d');
|
||||
this.test.assertEquals(this.get_cells_length(), 3, "d, 1 second wait, d doesn't delete a cell");
|
||||
this.validate_notebook_state('d, 1 second wait, d', 'command', 2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var b = 'print("b")';
|
||||
index = this.append_cell(b);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var c = 'print("c")';
|
||||
index = this.append_cell(c);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
|
||||
// Up and down in command mode
|
||||
this.select_cell(3);
|
||||
this.trigger_keydown('j');
|
||||
this.validate_notebook_state('j at end of notebook', 'command', 3);
|
||||
this.trigger_keydown('down');
|
||||
this.validate_notebook_state('down at end of notebook', 'command', 3);
|
||||
this.trigger_keydown('up');
|
||||
this.validate_notebook_state('up', 'command', 2);
|
||||
this.select_cell(0);
|
||||
this.validate_notebook_state('select 0', 'command', 0);
|
||||
this.trigger_keydown('k');
|
||||
this.validate_notebook_state('k at top of notebook', 'command', 0);
|
||||
this.trigger_keydown('up');
|
||||
this.validate_notebook_state('up at top of notebook', 'command', 0);
|
||||
this.trigger_keydown('down');
|
||||
this.validate_notebook_state('down', 'command', 1);
|
||||
|
||||
// Up and down in edit mode
|
||||
this.click_cell_editor(3);
|
||||
this.validate_notebook_state('click cell 3', 'edit', 3);
|
||||
this.trigger_keydown('down');
|
||||
this.validate_notebook_state('down at end of notebook', 'edit', 3);
|
||||
this.set_cell_editor_cursor(3, 0, 0);
|
||||
this.trigger_keydown('up');
|
||||
this.validate_notebook_state('up', 'edit', 2);
|
||||
this.click_cell_editor(0);
|
||||
this.validate_notebook_state('click 0', 'edit', 0);
|
||||
this.trigger_keydown('up');
|
||||
this.validate_notebook_state('up at top of notebook', 'edit', 0);
|
||||
this.set_cell_editor_cursor(0, 0, 10);
|
||||
this.trigger_keydown('down');
|
||||
this.validate_notebook_state('down', 'edit', 1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var b = 'print("b")';
|
||||
index = this.append_cell(b);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var c = 'print("c")';
|
||||
index = this.append_cell(c);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
// Cell insertion
|
||||
this.select_cell(2);
|
||||
this.trigger_keydown('a'); // Creates one cell
|
||||
this.test.assertEquals(this.get_cell_text(2), '', 'a; New cell 2 text is empty');
|
||||
this.validate_notebook_state('a', 'command', 2);
|
||||
this.trigger_keydown('b'); // Creates one cell
|
||||
this.test.assertEquals(this.get_cell_text(2), '', 'b; Cell 2 text is still empty');
|
||||
this.test.assertEquals(this.get_cell_text(3), '', 'b; New cell 3 text is empty');
|
||||
this.validate_notebook_state('b', 'command', 3);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
// Test keyboard shortcuts that change the cell's mode.
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
this.then(function () {
|
||||
// Cell mode change
|
||||
this.select_cell(0);
|
||||
this.trigger_keydown('esc','r');
|
||||
this.test.assertEquals(this.get_cell(0).cell_type, 'raw', 'r; cell is raw');
|
||||
this.trigger_keydown('1');
|
||||
this.test.assertEquals(this.get_cell(0).cell_type, 'heading', '1; cell is heading');
|
||||
this.test.assertEquals(this.get_cell(0).level, 1, '1; cell is level 1 heading');
|
||||
this.trigger_keydown('2');
|
||||
this.test.assertEquals(this.get_cell(0).level, 2, '2; cell is level 2 heading');
|
||||
this.trigger_keydown('3');
|
||||
this.test.assertEquals(this.get_cell(0).level, 3, '3; cell is level 3 heading');
|
||||
this.trigger_keydown('4');
|
||||
this.test.assertEquals(this.get_cell(0).level, 4, '4; cell is level 4 heading');
|
||||
this.trigger_keydown('5');
|
||||
this.test.assertEquals(this.get_cell(0).level, 5, '5; cell is level 5 heading');
|
||||
this.trigger_keydown('6');
|
||||
this.test.assertEquals(this.get_cell(0).level, 6, '6; cell is level 6 heading');
|
||||
this.trigger_keydown('m');
|
||||
this.test.assertEquals(this.get_cell(0).cell_type, 'markdown', 'm; cell is markdown');
|
||||
this.trigger_keydown('y');
|
||||
this.test.assertEquals(this.get_cell(0).cell_type, 'code', 'y; cell is code');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var b = 'print("b")';
|
||||
index = this.append_cell(b);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var c = 'print("c")';
|
||||
index = this.append_cell(c);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
// Copy/paste/cut
|
||||
var num_cells = this.get_cells_length();
|
||||
this.test.assertEquals(this.get_cell_text(1), a, 'Verify that cell 1 is a');
|
||||
this.select_cell(1);
|
||||
this.trigger_keydown('x'); // Cut
|
||||
this.validate_notebook_state('x', 'command', 1);
|
||||
this.test.assertEquals(this.get_cells_length(), num_cells-1, 'Verify that a cell was removed.');
|
||||
this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 2 is now where cell 1 was.');
|
||||
this.select_cell(2);
|
||||
this.trigger_keydown('v'); // Paste
|
||||
this.validate_notebook_state('v', 'command', 3); // Selection should move to pasted cell, below current cell.
|
||||
this.test.assertEquals(this.get_cell_text(3), a, 'Verify that cell 3 has the cut contents.');
|
||||
this.test.assertEquals(this.get_cells_length(), num_cells, 'Verify a the cell was added.');
|
||||
this.trigger_keydown('v'); // Paste
|
||||
this.validate_notebook_state('v', 'command', 4); // Selection should move to pasted cell, below current cell.
|
||||
this.test.assertEquals(this.get_cell_text(4), a, 'Verify that cell 4 has the cut contents.');
|
||||
this.test.assertEquals(this.get_cells_length(), num_cells+1, 'Verify a the cell was added.');
|
||||
this.select_cell(1);
|
||||
this.trigger_keydown('c'); // Copy
|
||||
this.validate_notebook_state('c', 'command', 1);
|
||||
this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 1 is b');
|
||||
this.select_cell(2);
|
||||
this.trigger_keydown('c'); // Copy
|
||||
this.validate_notebook_state('c', 'command', 2);
|
||||
this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 is c');
|
||||
this.select_cell(4);
|
||||
this.trigger_keydown('v'); // Paste
|
||||
this.validate_notebook_state('v', 'command', 5);
|
||||
this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 still has the copied contents.');
|
||||
this.test.assertEquals(this.get_cell_text(5), c, 'Verify that cell 5 has the copied contents.');
|
||||
this.test.assertEquals(this.get_cells_length(), num_cells+2, 'Verify a the cell was added.');
|
||||
this.select_cell(0);
|
||||
this.trigger_keydown('shift-v'); // Paste
|
||||
this.validate_notebook_state('shift-v', 'command', 0);
|
||||
this.test.assertEquals(this.get_cell_text(0), c, 'Verify that cell 0 has the copied contents.');
|
||||
this.test.assertEquals(this.get_cells_length(), num_cells+3, 'Verify a the cell was added.');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,72 @@
|
||||
// Test keyboard invoked execution.
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var b = 'print("b")';
|
||||
index = this.append_cell(b);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
var c = 'print("c")';
|
||||
index = this.append_cell(c);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
|
||||
// shift-enter
|
||||
// last cell in notebook
|
||||
var base_index = 3;
|
||||
this.select_cell(base_index);
|
||||
this.trigger_keydown('shift-enter'); // Creates one cell
|
||||
this.validate_notebook_state('shift-enter (no cell below)', 'edit', base_index + 1);
|
||||
// not last cell in notebook & starts in edit mode
|
||||
this.click_cell_editor(base_index);
|
||||
this.validate_notebook_state('click cell ' + base_index, 'edit', base_index);
|
||||
this.trigger_keydown('shift-enter');
|
||||
this.validate_notebook_state('shift-enter (cell exists below)', 'command', base_index + 1);
|
||||
// starts in command mode
|
||||
this.trigger_keydown('k');
|
||||
this.validate_notebook_state('k in comand mode', 'command', base_index);
|
||||
this.trigger_keydown('shift-enter');
|
||||
this.validate_notebook_state('shift-enter (start in command mode)', 'command', base_index + 1);
|
||||
|
||||
// ctrl-enter
|
||||
// last cell in notebook
|
||||
base_index++;
|
||||
this.trigger_keydown('ctrl-enter');
|
||||
this.validate_notebook_state('ctrl-enter (no cell below)', 'command', base_index);
|
||||
// not last cell in notebook & starts in edit mode
|
||||
this.click_cell_editor(base_index-1);
|
||||
this.validate_notebook_state('click cell ' + (base_index-1), 'edit', base_index-1);
|
||||
this.trigger_keydown('ctrl-enter');
|
||||
this.validate_notebook_state('ctrl-enter (cell exists below)', 'command', base_index-1);
|
||||
// starts in command mode
|
||||
this.trigger_keydown('j');
|
||||
this.validate_notebook_state('j in comand mode', 'command', base_index);
|
||||
this.trigger_keydown('ctrl-enter');
|
||||
this.validate_notebook_state('ctrl-enter (start in command mode)', 'command', base_index);
|
||||
|
||||
// alt-enter
|
||||
// last cell in notebook
|
||||
this.trigger_keydown('alt-enter'); // Creates one cell
|
||||
this.validate_notebook_state('alt-enter (no cell below)', 'edit', base_index + 1);
|
||||
// not last cell in notebook & starts in edit mode
|
||||
this.click_cell_editor(base_index);
|
||||
this.validate_notebook_state('click cell ' + base_index, 'edit', base_index);
|
||||
this.trigger_keydown('alt-enter'); // Creates one cell
|
||||
this.validate_notebook_state('alt-enter (cell exists below)', 'edit', base_index + 1);
|
||||
// starts in command mode
|
||||
this.trigger_keydown('esc', 'k');
|
||||
this.validate_notebook_state('k in comand mode', 'command', base_index);
|
||||
this.trigger_keydown('alt-enter'); // Creates one cell
|
||||
this.validate_notebook_state('alt-enter (start in command mode)', 'edit', base_index + 1);
|
||||
|
||||
// Notebook will now have 8 cells, the index of the last cell will be 7.
|
||||
this.test.assertEquals(this.get_cells_length(), 8, '*-enter commands added cells where needed.');
|
||||
this.select_cell(7);
|
||||
this.validate_notebook_state('click cell ' + 7 + ' and esc', 'command', 7);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,39 @@
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
var a = 'print("a")';
|
||||
var index = this.append_cell(a);
|
||||
this.execute_cell_then(index);
|
||||
|
||||
this.then(function () {
|
||||
// Markdown rendering / unredering
|
||||
this.select_cell(1);
|
||||
this.validate_notebook_state('select 1', 'command', 1);
|
||||
this.trigger_keydown('m');
|
||||
this.test.assertEquals(this.get_cell(1).cell_type, 'markdown', 'm; cell is markdown');
|
||||
this.test.assertEquals(this.get_cell(1).rendered, false, 'm; cell is rendered');
|
||||
this.trigger_keydown('enter');
|
||||
this.test.assertEquals(this.get_cell(1).rendered, false, 'enter; cell is unrendered');
|
||||
this.validate_notebook_state('enter', 'edit', 1);
|
||||
this.trigger_keydown('ctrl-enter');
|
||||
this.test.assertEquals(this.get_cell(1).rendered, true, 'ctrl-enter; cell is rendered');
|
||||
this.validate_notebook_state('enter', 'command', 1);
|
||||
this.trigger_keydown('enter');
|
||||
this.test.assertEquals(this.get_cell(1).rendered, false, 'enter; cell is unrendered');
|
||||
this.select_cell(0);
|
||||
this.test.assertEquals(this.get_cell(1).rendered, false, 'select 0; cell 1 is still unrendered');
|
||||
this.validate_notebook_state('select 0', 'command', 0);
|
||||
this.select_cell(1);
|
||||
this.validate_notebook_state('select 1', 'command', 1);
|
||||
this.trigger_keydown('ctrl-enter');
|
||||
this.test.assertEquals(this.get_cell(1).rendered, true, 'ctrl-enter; cell is rendered');
|
||||
this.select_cell(0);
|
||||
this.validate_notebook_state('select 0', 'command', 0);
|
||||
this.trigger_keydown('shift-enter');
|
||||
this.validate_notebook_state('shift-enter', 'command', 1);
|
||||
this.test.assertEquals(this.get_cell(1).rendered, true, 'shift-enter; cell is rendered');
|
||||
this.trigger_keydown('shift-enter'); // Creates one cell
|
||||
this.validate_notebook_state('shift-enter', 'edit', 2);
|
||||
this.test.assertEquals(this.get_cell(1).rendered, true, 'shift-enter; cell is rendered');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
|
||||
// Test
|
||||
casper.notebook_test(function () {
|
||||
this.then(function () {
|
||||
// Split and merge cells
|
||||
this.select_cell(0);
|
||||
this.trigger_keydown('a', 'enter'); // Create cell above and enter edit mode.
|
||||
this.validate_notebook_state('a, enter', 'edit', 0);
|
||||
this.set_cell_text(0, 'abcd');
|
||||
this.set_cell_editor_cursor(0, 0, 2);
|
||||
this.test.assertEquals(this.get_cell_text(0), 'abcd', 'Verify that cell 0 has the new contents.');
|
||||
this.trigger_keydown('ctrl-shift-subtract'); // Split
|
||||
this.test.assertEquals(this.get_cell_text(0), 'ab', 'split; Verify that cell 0 has the first half.');
|
||||
this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.');
|
||||
this.validate_notebook_state('split', 'edit', 1);
|
||||
this.select_cell(0); // Move up to cell 0
|
||||
this.trigger_keydown('shift-m'); // Merge
|
||||
this.validate_notebook_state('merge', 'command', 0);
|
||||
this.test.assertEquals(this.get_cell_text(0), 'ab\ncd', 'merge; Verify that cell 0 has the merged contents.');
|
||||
});
|
||||
});
|
||||
@ -1,38 +0,0 @@
|
||||
//
|
||||
// Test merging two notebook cells.
|
||||
//
|
||||
casper.notebook_test(function() {
|
||||
var output = this.evaluate(function () {
|
||||
// Fill in test data.
|
||||
IPython.notebook.command_mode();
|
||||
var set_cell_text = function () {
|
||||
var cell_one = IPython.notebook.get_selected_cell();
|
||||
cell_one.set_text('a = 5');
|
||||
|
||||
IPython.keyboard.trigger_keydown('b');
|
||||
var cell_two = IPython.notebook.get_selected_cell();
|
||||
cell_two.set_text('print(a)');
|
||||
};
|
||||
|
||||
// merge_cell_above()
|
||||
set_cell_text();
|
||||
IPython.notebook.merge_cell_above();
|
||||
var merged_above = IPython.notebook.get_selected_cell();
|
||||
|
||||
// merge_cell_below()
|
||||
set_cell_text();
|
||||
IPython.notebook.select(0);
|
||||
IPython.notebook.merge_cell_below();
|
||||
var merged_below = IPython.notebook.get_selected_cell();
|
||||
|
||||
return {
|
||||
above: merged_above.get_text(),
|
||||
below: merged_below.get_text()
|
||||
};
|
||||
});
|
||||
|
||||
this.test.assertEquals(output.above, 'a = 5\nprint(a)',
|
||||
'Successful merge_cell_above().');
|
||||
this.test.assertEquals(output.below, 'a = 5\nprint(a)',
|
||||
'Successful merge_cell_below().');
|
||||
});
|
||||
@ -0,0 +1,43 @@
|
||||
//
|
||||
// Test merging two notebook cells.
|
||||
//
|
||||
casper.notebook_test(function() {
|
||||
var that = this;
|
||||
var set_cells_text = function () {
|
||||
that.evaluate(function() {
|
||||
var cell_one = IPython.notebook.get_selected_cell();
|
||||
cell_one.set_text('a = 5');
|
||||
});
|
||||
|
||||
that.trigger_keydown('b');
|
||||
|
||||
that.evaluate(function() {
|
||||
var cell_two = IPython.notebook.get_selected_cell();
|
||||
cell_two.set_text('print(a)');
|
||||
});
|
||||
};
|
||||
|
||||
this.evaluate(function () {
|
||||
IPython.notebook.command_mode();
|
||||
});
|
||||
|
||||
// merge_cell_above()
|
||||
set_cells_text();
|
||||
var output_above = this.evaluate(function () {
|
||||
IPython.notebook.merge_cell_above();
|
||||
return IPython.notebook.get_selected_cell().get_text();
|
||||
});
|
||||
|
||||
// merge_cell_below()
|
||||
set_cells_text();
|
||||
var output_below = this.evaluate(function() {
|
||||
IPython.notebook.select(0);
|
||||
IPython.notebook.merge_cell_below();
|
||||
return IPython.notebook.get_selected_cell().get_text();
|
||||
});
|
||||
|
||||
this.test.assertEquals(output_above, 'a = 5\nprint(a)',
|
||||
'Successful merge_cell_above().');
|
||||
this.test.assertEquals(output_below, 'a = 5\nprint(a)',
|
||||
'Successful merge_cell_below().');
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
====================
|
||||
The IPython notebook
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
notebook
|
||||
cm_keyboard
|
||||
nbconvert
|
||||
public_server
|
||||
security
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v2.0.22 (GNU/Linux)
|
||||
|
||||
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
|
||||
5vmLc+Ibz+eT+rM=
|
||||
=XVDe
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@ -0,0 +1,146 @@
|
||||
Security in IPython notebooks
|
||||
=============================
|
||||
|
||||
As IPython notebooks become more popular for sharing and collaboration,
|
||||
the potential for malicious people to attempt to exploit the notebook
|
||||
for their nefarious purposes increases. IPython 2.0 introduces a
|
||||
security model to prevent execution of untrusted code without explicit
|
||||
user input.
|
||||
|
||||
The problem
|
||||
-----------
|
||||
|
||||
The whole point of IPython is arbitrary code execution. We have no
|
||||
desire to limit what can be done with a notebook, which would negatively
|
||||
impact its utility.
|
||||
|
||||
Unlike other programs, an IPython notebook document includes output.
|
||||
Unlike other documents, that output exists in a context that can execute
|
||||
code (via Javascript).
|
||||
|
||||
The security problem we need to solve is that no code should execute
|
||||
just because a user has **opened** a notebook that **they did not
|
||||
write**. Like any other program, once a user decides to execute code in
|
||||
a notebook, it is considered trusted, and should be allowed to do
|
||||
anything.
|
||||
|
||||
Our security model
|
||||
------------------
|
||||
|
||||
- Untrusted HTML is always sanitized
|
||||
- Untrusted Javascript is never executed
|
||||
- HTML and Javascript in Markdown cells are never trusted
|
||||
- **Outputs** generated by the user are trusted
|
||||
- Any other HTML or Javascript (in Markdown cells, output generated by
|
||||
others) is never trusted
|
||||
- The central question of trust is "Did the current user do this?"
|
||||
|
||||
The details of trust
|
||||
--------------------
|
||||
|
||||
IPython notebooks store a signature in metadata, which is used to answer
|
||||
the question "Did the current user do this?"
|
||||
|
||||
This signature is a digest of the notebooks contents plus a secret key,
|
||||
known only to the user. The secret key is a user-only readable file in
|
||||
the IPython profile's security directory. By default, this is::
|
||||
|
||||
~/.ipython/profile_default/security/notebook_secret
|
||||
|
||||
When a notebook is opened by a user, the server computes a signature
|
||||
with the user's key, and compares it with the signature stored in the
|
||||
notebook's metadata. If the signature matches, HTML and Javascript
|
||||
output in the notebook will be trusted at load, otherwise it will be
|
||||
untrusted.
|
||||
|
||||
Any output generated during an interactive session is trusted.
|
||||
|
||||
Updating trust
|
||||
**************
|
||||
|
||||
A notebook's trust is updated when the notebook is saved. If there are
|
||||
any untrusted outputs still in the notebook, the notebook will not be
|
||||
trusted, and no signature will be stored. If all untrusted outputs have
|
||||
been removed (either via ``Clear Output`` or re-execution), then the
|
||||
notebook will become trusted.
|
||||
|
||||
While trust is updated per output, this is only for the duration of a
|
||||
single session. A notebook file on disk is either trusted or not in its
|
||||
entirety.
|
||||
|
||||
Explicit trust
|
||||
**************
|
||||
|
||||
Sometimes re-executing a notebook to generate trusted output is not an
|
||||
option, either because dependencies are unavailable, or it would take a
|
||||
long time. Users can explicitly trust a notebook in two ways:
|
||||
|
||||
- At the command-line, with::
|
||||
|
||||
ipython trust /path/to/notebook.ipynb
|
||||
|
||||
- After loading the untrusted notebook, with ``File / Trust Notebook``
|
||||
|
||||
These two methods simply load the notebook, compute a new signature with
|
||||
the user's key, and then store the newly signed notebook.
|
||||
|
||||
Reporting security issues
|
||||
-------------------------
|
||||
|
||||
If you find a security vulnerability in IPython, either a failure of the
|
||||
code to properly implement the model described here, or a failure of the
|
||||
model itself, please report it to security@ipython.org.
|
||||
|
||||
If you prefer to encrypt your security reports,
|
||||
you can use :download:`this PGP public key <ipython_security.asc>`.
|
||||
|
||||
Affected use cases
|
||||
------------------
|
||||
|
||||
Some use cases that work in IPython 1.0 will become less convenient in
|
||||
2.0 as a result of the security changes. We do our best to minimize
|
||||
these annoyance, but security is always at odds with convenience.
|
||||
|
||||
Javascript and CSS in Markdown cells
|
||||
************************************
|
||||
|
||||
While never officially supported, it had become common practice to put
|
||||
hidden Javascript or CSS styling in Markdown cells, so that they would
|
||||
not be visible on the page. Since Markdown cells are now sanitized (by
|
||||
`Google Caja <https://developers.google.com/caja>`__), all Javascript
|
||||
(including click event handlers, etc.) and CSS will be stripped.
|
||||
|
||||
We plan to provide a mechanism for notebook themes, but in the meantime
|
||||
styling the notebook can only be done via either ``custom.css`` or CSS
|
||||
in HTML output. The latter only have an effect if the notebook is
|
||||
trusted, because otherwise the output will be sanitized just like
|
||||
Markdown.
|
||||
|
||||
Collaboration
|
||||
*************
|
||||
|
||||
When collaborating on a notebook, people probably want to see the
|
||||
outputs produced by their colleagues' most recent executions. Since each
|
||||
collaborator's key will differ, this will result in each share starting
|
||||
in an untrusted state. There are three basic approaches to this:
|
||||
|
||||
- re-run notebooks when you get them (not always viable)
|
||||
- explicitly trust notebooks via ``ipython trust`` or the notebook menu
|
||||
(annoying, but easy)
|
||||
- share a notebook secret, and use an IPython profile dedicated to the
|
||||
collaboration while working on the project.
|
||||
|
||||
Multiple profiles or machines
|
||||
*****************************
|
||||
|
||||
Since the notebook secret is stored in a profile directory by default,
|
||||
opening a notebook with a different profile or on a different machine
|
||||
will result in a different key, and thus be untrusted. The only current
|
||||
way to address this is by sharing the notebook secret. This can be
|
||||
facilitated by setting the configurable:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
c.NotebookApp.secret_file = "/path/to/notebook_secret"
|
||||
|
||||
in each profile, and only sharing the secret once per machine.
|
||||
Loading…
Reference in new issue