##// END OF EJS Templates
Merge pull request #4824 from minrk/sign-notebooks...
Brian E. Granger -
r14932:c0082785 merge
parent child Browse files
Show More
@@ -0,0 +1,277 b''
1 """Functions for signing notebooks"""
2 #-----------------------------------------------------------------------------
3 # Copyright (C) 2014, The IPython Development Team
4 #
5 # Distributed under the terms of the BSD License. The full license is in
6 # the file COPYING, distributed as part of this software.
7 #-----------------------------------------------------------------------------
8
9 #-----------------------------------------------------------------------------
10 # Imports
11 #-----------------------------------------------------------------------------
12
13 import base64
14 from contextlib import contextmanager
15 import hashlib
16 from hmac import HMAC
17 import io
18 import os
19
20 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
21 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool
22 from IPython.config import LoggingConfigurable, MultipleInstanceError
23 from IPython.core.application import BaseIPythonApplication, base_flags
24
25 from .current import read, write
26
27 #-----------------------------------------------------------------------------
28 # Code
29 #-----------------------------------------------------------------------------
30 try:
31 # Python 3
32 algorithms = hashlib.algorithms_guaranteed
33 except AttributeError:
34 algorithms = hashlib.algorithms
35
36 def yield_everything(obj):
37 """Yield every item in a container as bytes
38
39 Allows any JSONable object to be passed to an HMAC digester
40 without having to serialize the whole thing.
41 """
42 if isinstance(obj, dict):
43 for key in sorted(obj):
44 value = obj[key]
45 yield cast_bytes(key)
46 for b in yield_everything(value):
47 yield b
48 elif isinstance(obj, (list, tuple)):
49 for element in obj:
50 for b in yield_everything(element):
51 yield b
52 elif isinstance(obj, unicode_type):
53 yield obj.encode('utf8')
54 else:
55 yield unicode_type(obj).encode('utf8')
56
57
58 @contextmanager
59 def signature_removed(nb):
60 """Context manager for operating on a notebook with its signature removed
61
62 Used for excluding the previous signature when computing a notebook's signature.
63 """
64 save_signature = nb['metadata'].pop('signature', None)
65 try:
66 yield
67 finally:
68 if save_signature is not None:
69 nb['metadata']['signature'] = save_signature
70
71
72 class NotebookNotary(LoggingConfigurable):
73 """A class for computing and verifying notebook signatures."""
74
75 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
76 def _profile_dir_default(self):
77 from IPython.core.application import BaseIPythonApplication
78 app = None
79 try:
80 if BaseIPythonApplication.initialized():
81 app = BaseIPythonApplication.instance()
82 except MultipleInstanceError:
83 pass
84 if app is None:
85 # create an app, without the global instance
86 app = BaseIPythonApplication()
87 app.initialize()
88 return app.profile_dir
89
90 algorithm = Enum(algorithms, default_value='sha256', config=True,
91 help="""The hashing algorithm used to sign notebooks."""
92 )
93 def _algorithm_changed(self, name, old, new):
94 self.digestmod = getattr(hashlib, self.algorithm)
95
96 digestmod = Any()
97 def _digestmod_default(self):
98 return getattr(hashlib, self.algorithm)
99
100 secret_file = Unicode()
101 def _secret_file_default(self):
102 if self.profile_dir is None:
103 return ''
104 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
105
106 secret = Bytes(config=True,
107 help="""The secret key with which notebooks are signed."""
108 )
109 def _secret_default(self):
110 # note : this assumes an Application is running
111 if os.path.exists(self.secret_file):
112 with io.open(self.secret_file, 'rb') as f:
113 return f.read()
114 else:
115 secret = base64.encodestring(os.urandom(1024))
116 self._write_secret_file(secret)
117 return secret
118
119 def _write_secret_file(self, secret):
120 """write my secret to my secret_file"""
121 self.log.info("Writing notebook-signing key to %s", self.secret_file)
122 with io.open(self.secret_file, 'wb') as f:
123 f.write(secret)
124 try:
125 os.chmod(self.secret_file, 0o600)
126 except OSError:
127 self.log.warn(
128 "Could not set permissions on %s",
129 self.secret_file
130 )
131 return secret
132
133 def compute_signature(self, nb):
134 """Compute a notebook's signature
135
136 by hashing the entire contents of the notebook via HMAC digest.
137 """
138 hmac = HMAC(self.secret, digestmod=self.digestmod)
139 # don't include the previous hash in the content to hash
140 with signature_removed(nb):
141 # sign the whole thing
142 for b in yield_everything(nb):
143 hmac.update(b)
144
145 return hmac.hexdigest()
146
147 def check_signature(self, nb):
148 """Check a notebook's stored signature
149
150 If a signature is stored in the notebook's metadata,
151 a new signature is computed and compared with the stored value.
152
153 Returns True if the signature is found and matches, False otherwise.
154
155 The following conditions must all be met for a notebook to be trusted:
156 - a signature is stored in the form 'scheme:hexdigest'
157 - the stored scheme matches the requested scheme
158 - the requested scheme is available from hashlib
159 - the computed hash from notebook_signature matches the stored hash
160 """
161 stored_signature = nb['metadata'].get('signature', None)
162 if not stored_signature \
163 or not isinstance(stored_signature, string_types) \
164 or ':' not in stored_signature:
165 return False
166 stored_algo, sig = stored_signature.split(':', 1)
167 if self.algorithm != stored_algo:
168 return False
169 my_signature = self.compute_signature(nb)
170 return my_signature == sig
171
172 def sign(self, nb):
173 """Sign a notebook, indicating that its output is trusted
174
175 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
176
177 e.g. 'sha256:deadbeef123...'
178 """
179 signature = self.compute_signature(nb)
180 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
181
182 def mark_cells(self, nb, trusted):
183 """Mark cells as trusted if the notebook's signature can be verified
184
185 Sets ``cell.trusted = True | False`` on all code cells,
186 depending on whether the stored signature can be verified.
187
188 This function is the inverse of check_cells
189 """
190 if not nb['worksheets']:
191 # nothing to mark if there are no cells
192 return
193 for cell in nb['worksheets'][0]['cells']:
194 if cell['cell_type'] == 'code':
195 cell['trusted'] = trusted
196
197 def check_cells(self, nb):
198 """Return whether all code cells are trusted
199
200 If there are no code cells, return True.
201
202 This function is the inverse of mark_cells.
203 """
204 if not nb['worksheets']:
205 return True
206 for cell in nb['worksheets'][0]['cells']:
207 if cell['cell_type'] != 'code':
208 continue
209 if not cell.get('trusted', False):
210 return False
211 return True
212
213
214 trust_flags = {
215 'reset' : (
216 {'TrustNotebookApp' : { 'reset' : True}},
217 """Generate a new key for notebook signature.
218 All previously signed notebooks will become untrusted.
219 """
220 ),
221 }
222 trust_flags.update(base_flags)
223 trust_flags.pop('init')
224
225
226 class TrustNotebookApp(BaseIPythonApplication):
227
228 description="""Sign one or more IPython notebooks with your key,
229 to trust their dynamic (HTML, Javascript) output.
230
231 Otherwise, you will have to re-execute the notebook to see output.
232 """
233
234 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
235
236 flags = trust_flags
237
238 reset = Bool(False, config=True,
239 help="""If True, generate a new key for notebook signature.
240 After reset, all previously signed notebooks will become untrusted.
241 """
242 )
243
244 notary = Instance(NotebookNotary)
245 def _notary_default(self):
246 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
247
248 def sign_notebook(self, notebook_path):
249 if not os.path.exists(notebook_path):
250 self.log.error("Notebook missing: %s" % notebook_path)
251 self.exit(1)
252 with io.open(notebook_path, encoding='utf8') as f:
253 nb = read(f, 'json')
254 if self.notary.check_signature(nb):
255 print("Notebook already signed: %s" % notebook_path)
256 else:
257 print("Signing notebook: %s" % notebook_path)
258 self.notary.sign(nb)
259 with io.open(notebook_path, 'w', encoding='utf8') as f:
260 write(nb, f, 'json')
261
262 def generate_new_key(self):
263 """Generate a new notebook signature key"""
264 print("Generating new notebook key: %s" % self.notary.secret_file)
265 self.notary._write_secret_file(os.urandom(1024))
266
267 def start(self):
268 if self.reset:
269 self.generate_new_key()
270 return
271 if not self.extra_args:
272 self.log.critical("Specify at least one notebook to sign.")
273 self.exit(1)
274
275 for notebook_path in self.extra_args:
276 self.sign_notebook(notebook_path)
277
@@ -0,0 +1,108 b''
1 """Test Notebook signing"""
2 #-----------------------------------------------------------------------------
3 # Copyright (C) 2014, The IPython Development Team
4 #
5 # Distributed under the terms of the BSD License. The full license is in
6 # the file COPYING, distributed as part of this software.
7 #-----------------------------------------------------------------------------
8
9 #-----------------------------------------------------------------------------
10 # Imports
11 #-----------------------------------------------------------------------------
12
13 from .. import sign
14 from .base import TestsBase
15
16 from ..current import read
17 from IPython.core.getipython import get_ipython
18
19 #-----------------------------------------------------------------------------
20 # Classes and functions
21 #-----------------------------------------------------------------------------
22
23 class TestNotary(TestsBase):
24
25 def setUp(self):
26 self.notary = sign.NotebookNotary(
27 secret=b'secret',
28 profile_dir=get_ipython().profile_dir
29 )
30 with self.fopen(u'test3.ipynb', u'r') as f:
31 self.nb = read(f, u'json')
32
33 def test_algorithms(self):
34 last_sig = ''
35 for algo in sign.algorithms:
36 self.notary.algorithm = algo
37 self.notary.sign(self.nb)
38 sig = self.nb.metadata.signature
39 print(sig)
40 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
41 self.assertNotEqual(last_sig, sig)
42 last_sig = sig
43
44 def test_sign_same(self):
45 """Multiple signatures of the same notebook are the same"""
46 sig1 = self.notary.compute_signature(self.nb)
47 sig2 = self.notary.compute_signature(self.nb)
48 self.assertEqual(sig1, sig2)
49
50 def test_change_secret(self):
51 """Changing the secret changes the signature"""
52 sig1 = self.notary.compute_signature(self.nb)
53 self.notary.secret = b'different'
54 sig2 = self.notary.compute_signature(self.nb)
55 self.assertNotEqual(sig1, sig2)
56
57 def test_sign(self):
58 self.notary.sign(self.nb)
59 sig = self.nb.metadata.signature
60 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
61
62 def test_check_signature(self):
63 nb = self.nb
64 md = nb.metadata
65 notary = self.notary
66 check_signature = notary.check_signature
67 # no signature:
68 md.pop('signature', None)
69 self.assertFalse(check_signature(nb))
70 # hash only, no algo
71 md.signature = notary.compute_signature(nb)
72 self.assertFalse(check_signature(nb))
73 # proper signature, algo mismatch
74 notary.algorithm = 'sha224'
75 notary.sign(nb)
76 notary.algorithm = 'sha256'
77 self.assertFalse(check_signature(nb))
78 # check correctly signed notebook
79 notary.sign(nb)
80 self.assertTrue(check_signature(nb))
81
82 def test_mark_cells_untrusted(self):
83 cells = self.nb.worksheets[0].cells
84 self.notary.mark_cells(self.nb, False)
85 for cell in cells:
86 if cell.cell_type == 'code':
87 self.assertIn('trusted', cell)
88 self.assertFalse(cell.trusted)
89 else:
90 self.assertNotIn('trusted', cell)
91
92 def test_mark_cells_trusted(self):
93 cells = self.nb.worksheets[0].cells
94 self.notary.mark_cells(self.nb, True)
95 for cell in cells:
96 if cell.cell_type == 'code':
97 self.assertIn('trusted', cell)
98 self.assertTrue(cell.trusted)
99 else:
100 self.assertNotIn('trusted', cell)
101
102 def test_check_cells(self):
103 nb = self.nb
104 self.notary.mark_cells(nb, True)
105 self.assertTrue(self.notary.check_cells(nb))
106 self.notary.mark_cells(nb, False)
107 self.assertFalse(self.notary.check_cells(nb))
108
@@ -0,0 +1,7 b''
1 Signing Notebooks
2 -----------------
3
4 To prevent untrusted code from executing on users' behalf when notebooks open,
5 we have added a signature to the notebook, stored in metadata.
6
7 For more information, see :ref:`signing_notebooks`.
@@ -207,12 +207,13 b' class FileNotebookManager(NotebookManager):'
207 model['path'] = path
207 model['path'] = path
208 model['last_modified'] = last_modified
208 model['last_modified'] = last_modified
209 model['created'] = created
209 model['created'] = created
210 if content is True:
210 if content:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 try:
212 try:
213 nb = current.read(f, u'json')
213 nb = current.read(f, u'json')
214 except Exception as e:
214 except Exception as e:
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 self.mark_trusted_cells(nb, path, name)
216 model['content'] = nb
217 model['content'] = nb
217 return model
218 return model
218
219
@@ -236,6 +237,9 b' class FileNotebookManager(NotebookManager):'
236 # Save the notebook file
237 # Save the notebook file
237 os_path = self.get_os_path(new_name, new_path)
238 os_path = self.get_os_path(new_name, new_path)
238 nb = current.to_notebook_json(model['content'])
239 nb = current.to_notebook_json(model['content'])
240
241 self.check_and_sign(nb, new_path, new_name)
242
239 if 'name' in nb['metadata']:
243 if 'name' in nb['metadata']:
240 nb['metadata']['name'] = u''
244 nb['metadata']['name'] = u''
241 try:
245 try:
@@ -20,9 +20,9 b' Authors:'
20 import os
20 import os
21
21
22 from IPython.config.configurable import LoggingConfigurable
22 from IPython.config.configurable import LoggingConfigurable
23 from IPython.nbformat import current
23 from IPython.nbformat import current, sign
24 from IPython.utils import py3compat
24 from IPython.utils import py3compat
25 from IPython.utils.traitlets import Unicode, TraitError
25 from IPython.utils.traitlets import Instance, Unicode, TraitError
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Classes
28 # Classes
@@ -42,6 +42,30 b' class NotebookManager(LoggingConfigurable):'
42
42
43 filename_ext = Unicode(u'.ipynb')
43 filename_ext = Unicode(u'.ipynb')
44
44
45 notary = Instance(sign.NotebookNotary)
46 def _notary_default(self):
47 return sign.NotebookNotary(parent=self)
48
49 def check_and_sign(self, nb, path, name):
50 """Check for trusted cells, and sign the notebook.
51
52 Called as a part of saving notebooks.
53 """
54 if self.notary.check_cells(nb):
55 self.notary.sign(nb)
56 else:
57 self.log.warn("Saving untrusted notebook %s/%s", path, name)
58
59 def mark_trusted_cells(self, nb, path, name):
60 """Mark cells as trusted if the notebook signature matches.
61
62 Called as a part of loading notebooks.
63 """
64 trusted = self.notary.check_signature(nb)
65 if not trusted:
66 self.log.warn("Notebook %s/%s is not trusted", path, name)
67 self.notary.mark_cells(nb, trusted)
68
45 def path_exists(self, path):
69 def path_exists(self, path):
46 """Does the API-style path (directory) actually exist?
70 """Does the API-style path (directory) actually exist?
47
71
@@ -530,6 +530,7 b' var IPython = (function (IPython) {'
530 } else {
530 } else {
531 this.set_input_prompt();
531 this.set_input_prompt();
532 }
532 }
533 this.output_area.trusted = data.trusted || false;
533 this.output_area.fromJSON(data.outputs);
534 this.output_area.fromJSON(data.outputs);
534 if (data.collapsed !== undefined) {
535 if (data.collapsed !== undefined) {
535 if (data.collapsed) {
536 if (data.collapsed) {
@@ -552,6 +553,7 b' var IPython = (function (IPython) {'
552 var outputs = this.output_area.toJSON();
553 var outputs = this.output_area.toJSON();
553 data.outputs = outputs;
554 data.outputs = outputs;
554 data.language = 'python';
555 data.language = 'python';
556 data.trusted = this.output_area.trusted;
555 data.collapsed = this.collapsed;
557 data.collapsed = this.collapsed;
556 return data;
558 return data;
557 };
559 };
@@ -31,6 +31,7 b' var IPython = (function (IPython) {'
31 this.outputs = [];
31 this.outputs = [];
32 this.collapsed = false;
32 this.collapsed = false;
33 this.scrolled = false;
33 this.scrolled = false;
34 this.trusted = true;
34 this.clear_queued = null;
35 this.clear_queued = null;
35 if (prompt_area === undefined) {
36 if (prompt_area === undefined) {
36 this.prompt_area = true;
37 this.prompt_area = true;
@@ -309,7 +310,7 b' var IPython = (function (IPython) {'
309 });
310 });
310 return json;
311 return json;
311 };
312 };
312
313
313 OutputArea.prototype.append_output = function (json) {
314 OutputArea.prototype.append_output = function (json) {
314 this.expand();
315 this.expand();
315 // Clear the output if clear is queued.
316 // Clear the output if clear is queued.
@@ -331,6 +332,7 b' var IPython = (function (IPython) {'
331 } else if (json.output_type === 'stream') {
332 } else if (json.output_type === 'stream') {
332 this.append_stream(json);
333 this.append_stream(json);
333 }
334 }
335
334 this.outputs.push(json);
336 this.outputs.push(json);
335
337
336 // Only reset the height to automatic if the height is currently
338 // Only reset the height to automatic if the height is currently
@@ -526,12 +528,26 b' var IPython = (function (IPython) {'
526 'text/plain'
528 'text/plain'
527 ];
529 ];
528
530
531 OutputArea.safe_outputs = {
532 'text/plain' : true,
533 'image/png' : true,
534 'image/jpeg' : true
535 };
536
529 OutputArea.prototype.append_mime_type = function (json, element) {
537 OutputArea.prototype.append_mime_type = function (json, element) {
530
531 for (var type_i in OutputArea.display_order) {
538 for (var type_i in OutputArea.display_order) {
532 var type = OutputArea.display_order[type_i];
539 var type = OutputArea.display_order[type_i];
533 var append = OutputArea.append_map[type];
540 var append = OutputArea.append_map[type];
534 if ((json[type] !== undefined) && append) {
541 if ((json[type] !== undefined) && append) {
542 if (!this.trusted && !OutputArea.safe_outputs[type]) {
543 // not trusted show warning and do not display
544 var content = {
545 text : "Untrusted " + type + " output ignored.",
546 stream : "stderr"
547 }
548 this.append_stream(content);
549 continue;
550 }
535 var md = json.metadata || {};
551 var md = json.metadata || {};
536 append.apply(this, [json[type], md, element]);
552 append.apply(this, [json[type], md, element]);
537 return true;
553 return true;
@@ -753,6 +769,7 b' var IPython = (function (IPython) {'
753 // clear all, no need for logic
769 // clear all, no need for logic
754 this.element.html("");
770 this.element.html("");
755 this.outputs = [];
771 this.outputs = [];
772 this.trusted = true;
756 this.unscroll_area();
773 this.unscroll_area();
757 return;
774 return;
758 };
775 };
@@ -765,13 +782,6 b' var IPython = (function (IPython) {'
765 var len = outputs.length;
782 var len = outputs.length;
766 var data;
783 var data;
767
784
768 // We don't want to display javascript on load, so remove it from the
769 // display order for the duration of this function call, but be sure to
770 // put it back in there so incoming messages that contain javascript
771 // representations get displayed
772 var js_index = OutputArea.display_order.indexOf('application/javascript');
773 OutputArea.display_order.splice(js_index, 1);
774
775 for (var i=0; i<len; i++) {
785 for (var i=0; i<len; i++) {
776 data = outputs[i];
786 data = outputs[i];
777 var msg_type = data.output_type;
787 var msg_type = data.output_type;
@@ -784,9 +794,6 b' var IPython = (function (IPython) {'
784
794
785 this.append_output(data);
795 this.append_output(data);
786 }
796 }
787
788 // reinsert javascript into display order, see note above
789 OutputArea.display_order.splice(js_index, 0, 'application/javascript');
790 };
797 };
791
798
792
799
@@ -27,9 +27,9 b' function assert_has(short_name, json, result, result2) {'
27 this.test.assertTrue(json[0].hasOwnProperty(short_name),
27 this.test.assertTrue(json[0].hasOwnProperty(short_name),
28 'toJSON() representation uses ' + short_name);
28 'toJSON() representation uses ' + short_name);
29 this.test.assertTrue(result.hasOwnProperty(long_name),
29 this.test.assertTrue(result.hasOwnProperty(long_name),
30 'toJSON() original embeded JSON keeps ' + long_name);
30 'toJSON() original embedded JSON keeps ' + long_name);
31 this.test.assertTrue(result2.hasOwnProperty(long_name),
31 this.test.assertTrue(result2.hasOwnProperty(long_name),
32 'fromJSON() embeded ' + short_name + ' gets mime key ' + long_name);
32 'fromJSON() embedded ' + short_name + ' gets mime key ' + long_name);
33 }
33 }
34
34
35 // helper function for checkout that the first two cells have a particular
35 // helper function for checkout that the first two cells have a particular
@@ -40,11 +40,11 b' function check_output_area(output_type, keys) {'
40 this.wait_for_output(0);
40 this.wait_for_output(0);
41 json = this.evaluate(function() {
41 json = this.evaluate(function() {
42 var json = IPython.notebook.get_cell(0).output_area.toJSON();
42 var json = IPython.notebook.get_cell(0).output_area.toJSON();
43 // appended cell will initially be empty, lets add it some output
43 // appended cell will initially be empty, let's add some output
44 var cell = IPython.notebook.get_cell(1).output_area.fromJSON(json);
44 IPython.notebook.get_cell(1).output_area.fromJSON(json);
45 return json;
45 return json;
46 });
46 });
47 // The evaluate call above happens asyncrhonously: wait for cell[1] to have output
47 // The evaluate call above happens asynchronously: wait for cell[1] to have output
48 this.wait_for_output(1);
48 this.wait_for_output(1);
49 var result = this.get_output_cell(0);
49 var result = this.get_output_cell(0);
50 var result2 = this.get_output_cell(1);
50 var result2 = this.get_output_cell(1);
@@ -88,12 +88,19 b' casper.notebook_test(function () {'
88 var num_cells = this.get_cells_length();
88 var num_cells = this.get_cells_length();
89 this.test.assertEquals(num_cells, 2, '%%javascript magic works');
89 this.test.assertEquals(num_cells, 2, '%%javascript magic works');
90 this.test.assertTrue(result.hasOwnProperty('application/javascript'),
90 this.test.assertTrue(result.hasOwnProperty('application/javascript'),
91 'testing JS embeded with mime key');
91 'testing JS embedded with mime key');
92 });
92 });
93
93
94 //this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
94 //this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
95 this.then(function () {
96 clear_and_execute(this, [
97 "%%javascript",
98 "var a=5;"
99 ].join('\n'));
100 });
101
95
102
96 this.then(function ( ) {
103 this.then(function () {
97 check_output_area.apply(this, ['display_data', ['javascript']]);
104 check_output_area.apply(this, ['display_data', ['javascript']]);
98
105
99 });
106 });
@@ -223,7 +230,9 b' casper.notebook_test(function () {'
223
230
224 });
231 });
225
232
226 this.then(function ( ) {
233 this.wait_for_output(0, 1);
234
235 this.then(function () {
227 var long_name = 'text/superfancymimetype';
236 var long_name = 'text/superfancymimetype';
228 var result = this.get_output_cell(0);
237 var result = this.get_output_cell(0);
229 this.test.assertTrue(result.hasOwnProperty(long_name),
238 this.test.assertTrue(result.hasOwnProperty(long_name),
@@ -35,8 +35,10 b' casper.notebook_test(function () {'
35 });
35 });
36
36
37 this.then(function () {
37 this.then(function () {
38 var result = this.get_output_cell(0);
38 var outputs = this.evaluate(function() {
39 this.test.assertFalsy(result, "after shutdown: no execution results");
39 return IPython.notebook.get_cell(0).output_area.outputs;
40 })
41 this.test.assertEquals(outputs.length, 0, "after shutdown: no execution results");
40 this.test.assertNot(this.kernel_running(),
42 this.test.assertNot(this.kernel_running(),
41 'after shutdown: IPython.notebook.kernel.running is false ');
43 'after shutdown: IPython.notebook.kernel.running is false ');
42 });
44 });
@@ -57,15 +57,18 b' casper.delete_current_notebook = function () {'
57 });
57 });
58 };
58 };
59
59
60 // wait for output in a given cell
60 // wait for the nth output in a given cell
61 casper.wait_for_output = function (cell_num) {
61 casper.wait_for_output = function (cell_num, out_num) {
62 this.waitFor(function (c) {
62 out_num = out_num || 0;
63 return this.evaluate(function get_output(c) {
63 this.then(function() {
64 var cell = IPython.notebook.get_cell(c);
64 this.waitFor(function (c, o) {
65 return cell.output_area.outputs.length != 0;
65 return this.evaluate(function get_output(c, o) {
66 },
66 var cell = IPython.notebook.get_cell(c);
67 // pass parameter from the test suite js to the browser code js
67 return cell.output_area.outputs.length > o;
68 {c : cell_num});
68 },
69 // pass parameter from the test suite js to the browser code js
70 {c : cell_num, o : out_num});
71 });
69 },
72 },
70 function then() { },
73 function then() { },
71 function timeout() {
74 function timeout() {
@@ -73,7 +76,7 b' casper.wait_for_output = function (cell_num) {'
73 });
76 });
74 };
77 };
75
78
76 // return the output of a given cell
79 // return an output of a given cell
77 casper.get_output_cell = function (cell_num, out_num) {
80 casper.get_output_cell = function (cell_num, out_num) {
78 out_num = out_num || 0;
81 out_num = out_num || 0;
79 var result = casper.evaluate(function (c, o) {
82 var result = casper.evaluate(function (c, o) {
@@ -81,7 +84,18 b' casper.get_output_cell = function (cell_num, out_num) {'
81 return cell.output_area.outputs[o];
84 return cell.output_area.outputs[o];
82 },
85 },
83 {c : cell_num, o : out_num});
86 {c : cell_num, o : out_num});
84 return result;
87 if (!result) {
88 var num_outputs = casper.evaluate(function (c) {
89 var cell = IPython.notebook.get_cell(c);
90 return cell.output_area.outputs.length;
91 },
92 {c : cell_num});
93 this.test.assertTrue(false,
94 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
95 );
96 } else {
97 return result;
98 }
85 };
99 };
86
100
87 // return the number of cells in the notebook
101 // return the number of cells in the notebook
@@ -249,6 +249,9 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):'
249 nbconvert=('IPython.nbconvert.nbconvertapp.NbConvertApp',
249 nbconvert=('IPython.nbconvert.nbconvertapp.NbConvertApp',
250 "Convert notebooks to/from other formats."
250 "Convert notebooks to/from other formats."
251 ),
251 ),
252 trust=('IPython.nbformat.sign.TrustNotebookApp',
253 "Sign notebooks to trust their potentially unsafe contents at load."
254 ),
252 ))
255 ))
253
256
254 # *do* autocreate requested profile, but don't create the config file.
257 # *do* autocreate requested profile, but don't create the config file.
@@ -35,3 +35,6 b' def test_locate_help():'
35
35
36 def test_locate_profile_help():
36 def test_locate_profile_help():
37 tt.help_all_output_test("locate profile")
37 tt.help_all_output_test("locate profile")
38
39 def test_trust_help():
40 tt.help_all_output_test("trust")
@@ -462,6 +462,35 b' on available options, use::'
462 :ref:`notebook_public_server`
462 :ref:`notebook_public_server`
463
463
464
464
465 .. _signing_notebooks:
466
467 Signing Notebooks
468 -----------------
469
470 To prevent untrusted code from executing on users' behalf when notebooks open,
471 we have added a signature to the notebook, stored in metadata.
472 The notebook server verifies this signature when a notebook is opened.
473 If the signature stored in the notebook metadata does not match,
474 javascript and HTML output will not be displayed on load,
475 and must be regenerated by re-executing the cells.
476
477 Any notebook that you have executed yourself *in its entirety* will be considered trusted,
478 and its HTML and javascript output will be displayed on load.
479
480 If you need to see HTML or Javascript output without re-executing,
481 you can explicitly trust notebooks, such as those shared with you,
482 or those that you have written yourself prior to IPython 2.0,
483 at the command-line with::
484
485 $ ipython trust mynotebook.ipynb [other notebooks.ipynb]
486
487 This just generates a new signature stored in each notebook.
488
489 You can generate a new notebook signing key with::
490
491 $ ipython trust --reset
492
493
465 Importing ``.py`` files
494 Importing ``.py`` files
466 -----------------------
495 -----------------------
467
496
General Comments 0
You need to be logged in to leave comments. Login now