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 |
|
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 |
|
43 | // appended cell will initially be empty, let's add some output | |
44 |
|
|
44 | IPython.notebook.get_cell(1).output_area.fromJSON(json); | |
45 | return json; |
|
45 | return json; | |
46 | }); |
|
46 | }); | |
47 |
// The evaluate call above happens async |
|
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 |
|
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 |
|
|
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