##// END OF EJS Templates
profile_dir
Sylvain Corlay -
Show More
@@ -1,396 +1,396 b''
1 1 # encoding: utf-8
2 2 """
3 3 An application for IPython.
4 4
5 5 All top-level applications should use the classes in this module for
6 6 handling configuration and creating configurables.
7 7
8 8 The job of an :class:`Application` is to create the master configuration
9 9 object and then create the configurable objects, passing the config to them.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15 15 import atexit
16 16 import glob
17 17 import logging
18 18 import os
19 19 import shutil
20 20 import sys
21 21
22 22 from IPython.config.application import Application, catch_config_error
23 23 from IPython.config.loader import ConfigFileNotFound, PyFileConfigLoader
24 24 from IPython.core import release, crashhandler
25 25 from IPython.core.profiledir import ProfileDir, ProfileDirError
26 26 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir, ensure_dir_exists
27 27 from IPython.utils import py3compat
28 28 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
29 29
30 30 if os.name == 'nt':
31 31 programdata = os.environ.get('PROGRAMDATA', None)
32 32 if programdata:
33 33 SYSTEM_CONFIG_DIRS = [os.path.join(programdata, 'ipython')]
34 34 else: # PROGRAMDATA is not defined by default on XP.
35 35 SYSTEM_CONFIG_DIRS = []
36 36 else:
37 37 SYSTEM_CONFIG_DIRS = [
38 38 "/usr/local/etc/ipython",
39 39 "/etc/ipython",
40 40 ]
41 41
42 42
43 43 # aliases and flags
44 44
45 45 base_aliases = {
46 46 'profile-dir' : 'ProfileDir.location',
47 47 'profile' : 'BaseIPythonApplication.profile',
48 48 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
49 49 'log-level' : 'Application.log_level',
50 50 'config' : 'BaseIPythonApplication.extra_config_file',
51 51 }
52 52
53 53 base_flags = dict(
54 54 debug = ({'Application' : {'log_level' : logging.DEBUG}},
55 55 "set log level to logging.DEBUG (maximize logging output)"),
56 56 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
57 57 "set log level to logging.CRITICAL (minimize logging output)"),
58 58 init = ({'BaseIPythonApplication' : {
59 59 'copy_config_files' : True,
60 60 'auto_create' : True}
61 61 }, """Initialize profile with default config files. This is equivalent
62 62 to running `ipython profile create <profile>` prior to startup.
63 63 """)
64 64 )
65 65
66 66 class ProfileAwareConfigLoader(PyFileConfigLoader):
67 67 """A Python file config loader that is aware of IPython profiles."""
68 68 def load_subconfig(self, fname, path=None, profile=None):
69 69 if profile is not None:
70 70 try:
71 71 profile_dir = ProfileDir.find_profile_dir_by_name(
72 72 get_ipython_dir(),
73 73 profile,
74 74 )
75 75 except ProfileDirError:
76 76 return
77 77 path = profile_dir.location
78 78 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
79 79
80 80 class BaseIPythonApplication(Application):
81 81
82 82 name = Unicode(u'ipython')
83 83 description = Unicode(u'IPython: an enhanced interactive Python shell.')
84 84 version = Unicode(release.version)
85 85
86 86 aliases = Dict(base_aliases)
87 87 flags = Dict(base_flags)
88 88 classes = List([ProfileDir])
89 89
90 90 # enable `load_subconfig('cfg.py', profile='name')`
91 91 python_config_loader_class = ProfileAwareConfigLoader
92 92
93 93 # Track whether the config_file has changed,
94 94 # because some logic happens only if we aren't using the default.
95 95 config_file_specified = Set()
96 96
97 97 config_file_name = Unicode()
98 98 def _config_file_name_default(self):
99 99 return self.name.replace('-','_') + u'_config.py'
100 100 def _config_file_name_changed(self, name, old, new):
101 101 if new != old:
102 102 self.config_file_specified.add(new)
103 103
104 104 # The directory that contains IPython's builtin profiles.
105 105 builtin_profile_dir = Unicode(
106 106 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
107 107 )
108 108
109 109 config_file_paths = List(Unicode)
110 110 def _config_file_paths_default(self):
111 111 return [py3compat.getcwd()]
112 112
113 113 extra_config_file = Unicode(config=True,
114 114 help="""Path to an extra config file to load.
115 115
116 116 If specified, load this config file in addition to any other IPython config.
117 117 """)
118 118 def _extra_config_file_changed(self, name, old, new):
119 119 try:
120 120 self.config_files.remove(old)
121 121 except ValueError:
122 122 pass
123 123 self.config_file_specified.add(new)
124 124 self.config_files.append(new)
125 125
126 126 profile = Unicode(u'default', config=True,
127 127 help="""The IPython profile to use."""
128 128 )
129 129
130 130 def _profile_changed(self, name, old, new):
131 131 self.builtin_profile_dir = os.path.join(
132 132 get_ipython_package_dir(), u'config', u'profile', new
133 133 )
134 134
135 135 ipython_dir = Unicode(config=True,
136 136 help="""
137 137 The name of the IPython directory. This directory is used for logging
138 138 configuration (through profiles), history storage, etc. The default
139 139 is usually $HOME/.ipython. This option can also be specified through
140 140 the environment variable IPYTHONDIR.
141 141 """
142 142 )
143 143 def _ipython_dir_default(self):
144 144 d = get_ipython_dir()
145 145 self._ipython_dir_changed('ipython_dir', d, d)
146 146 return d
147 147
148 148 _in_init_profile_dir = False
149 profile_dir = Instance(ProfileDir)
149 profile_dir = Instance(ProfileDir, allow_none=True)
150 150 def _profile_dir_default(self):
151 151 # avoid recursion
152 152 if self._in_init_profile_dir:
153 153 return
154 154 # profile_dir requested early, force initialization
155 155 self.init_profile_dir()
156 156 return self.profile_dir
157 157
158 158 overwrite = Bool(False, config=True,
159 159 help="""Whether to overwrite existing config files when copying""")
160 160 auto_create = Bool(False, config=True,
161 161 help="""Whether to create profile dir if it doesn't exist""")
162 162
163 163 config_files = List(Unicode)
164 164 def _config_files_default(self):
165 165 return [self.config_file_name]
166 166
167 167 copy_config_files = Bool(False, config=True,
168 168 help="""Whether to install the default config files into the profile dir.
169 169 If a new profile is being created, and IPython contains config files for that
170 170 profile, then they will be staged into the new directory. Otherwise,
171 171 default config files will be automatically generated.
172 172 """)
173 173
174 174 verbose_crash = Bool(False, config=True,
175 175 help="""Create a massive crash report when IPython encounters what may be an
176 176 internal error. The default is to append a short message to the
177 177 usual traceback""")
178 178
179 179 # The class to use as the crash handler.
180 180 crash_handler_class = Type(crashhandler.CrashHandler)
181 181
182 182 @catch_config_error
183 183 def __init__(self, **kwargs):
184 184 super(BaseIPythonApplication, self).__init__(**kwargs)
185 185 # ensure current working directory exists
186 186 try:
187 187 directory = py3compat.getcwd()
188 188 except:
189 189 # exit if cwd doesn't exist
190 190 self.log.error("Current working directory doesn't exist.")
191 191 self.exit(1)
192 192
193 193 #-------------------------------------------------------------------------
194 194 # Various stages of Application creation
195 195 #-------------------------------------------------------------------------
196 196
197 197 def init_crash_handler(self):
198 198 """Create a crash handler, typically setting sys.excepthook to it."""
199 199 self.crash_handler = self.crash_handler_class(self)
200 200 sys.excepthook = self.excepthook
201 201 def unset_crashhandler():
202 202 sys.excepthook = sys.__excepthook__
203 203 atexit.register(unset_crashhandler)
204 204
205 205 def excepthook(self, etype, evalue, tb):
206 206 """this is sys.excepthook after init_crashhandler
207 207
208 208 set self.verbose_crash=True to use our full crashhandler, instead of
209 209 a regular traceback with a short message (crash_handler_lite)
210 210 """
211 211
212 212 if self.verbose_crash:
213 213 return self.crash_handler(etype, evalue, tb)
214 214 else:
215 215 return crashhandler.crash_handler_lite(etype, evalue, tb)
216 216
217 217 def _ipython_dir_changed(self, name, old, new):
218 218 if old is not None:
219 219 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
220 220 sys.getfilesystemencoding()
221 221 )
222 222 if str_old in sys.path:
223 223 sys.path.remove(str_old)
224 224 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
225 225 sys.getfilesystemencoding()
226 226 )
227 227 sys.path.append(str_path)
228 228 ensure_dir_exists(new)
229 229 readme = os.path.join(new, 'README')
230 230 readme_src = os.path.join(get_ipython_package_dir(), u'config', u'profile', 'README')
231 231 if not os.path.exists(readme) and os.path.exists(readme_src):
232 232 shutil.copy(readme_src, readme)
233 233 for d in ('extensions', 'nbextensions'):
234 234 path = os.path.join(new, d)
235 235 try:
236 236 ensure_dir_exists(path)
237 237 except OSError:
238 238 # this will not be EEXIST
239 239 self.log.error("couldn't create path %s: %s", path, e)
240 240 self.log.debug("IPYTHONDIR set to: %s" % new)
241 241
242 242 def load_config_file(self, suppress_errors=True):
243 243 """Load the config file.
244 244
245 245 By default, errors in loading config are handled, and a warning
246 246 printed on screen. For testing, the suppress_errors option is set
247 247 to False, so errors will make tests fail.
248 248 """
249 249 self.log.debug("Searching path %s for config files", self.config_file_paths)
250 250 base_config = 'ipython_config.py'
251 251 self.log.debug("Attempting to load config file: %s" %
252 252 base_config)
253 253 try:
254 254 Application.load_config_file(
255 255 self,
256 256 base_config,
257 257 path=self.config_file_paths
258 258 )
259 259 except ConfigFileNotFound:
260 260 # ignore errors loading parent
261 261 self.log.debug("Config file %s not found", base_config)
262 262 pass
263 263
264 264 for config_file_name in self.config_files:
265 265 if not config_file_name or config_file_name == base_config:
266 266 continue
267 267 self.log.debug("Attempting to load config file: %s" %
268 268 self.config_file_name)
269 269 try:
270 270 Application.load_config_file(
271 271 self,
272 272 config_file_name,
273 273 path=self.config_file_paths
274 274 )
275 275 except ConfigFileNotFound:
276 276 # Only warn if the default config file was NOT being used.
277 277 if config_file_name in self.config_file_specified:
278 278 msg = self.log.warn
279 279 else:
280 280 msg = self.log.debug
281 281 msg("Config file not found, skipping: %s", config_file_name)
282 282 except:
283 283 # For testing purposes.
284 284 if not suppress_errors:
285 285 raise
286 286 self.log.warn("Error loading config file: %s" %
287 287 self.config_file_name, exc_info=True)
288 288
289 289 def init_profile_dir(self):
290 290 """initialize the profile dir"""
291 291 self._in_init_profile_dir = True
292 292 if self.profile_dir is not None:
293 293 # already ran
294 294 return
295 295 if 'ProfileDir.location' not in self.config:
296 296 # location not specified, find by profile name
297 297 try:
298 298 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
299 299 except ProfileDirError:
300 300 # not found, maybe create it (always create default profile)
301 301 if self.auto_create or self.profile == 'default':
302 302 try:
303 303 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
304 304 except ProfileDirError:
305 305 self.log.fatal("Could not create profile: %r"%self.profile)
306 306 self.exit(1)
307 307 else:
308 308 self.log.info("Created profile dir: %r"%p.location)
309 309 else:
310 310 self.log.fatal("Profile %r not found."%self.profile)
311 311 self.exit(1)
312 312 else:
313 313 self.log.debug("Using existing profile dir: %r"%p.location)
314 314 else:
315 315 location = self.config.ProfileDir.location
316 316 # location is fully specified
317 317 try:
318 318 p = ProfileDir.find_profile_dir(location, self.config)
319 319 except ProfileDirError:
320 320 # not found, maybe create it
321 321 if self.auto_create:
322 322 try:
323 323 p = ProfileDir.create_profile_dir(location, self.config)
324 324 except ProfileDirError:
325 325 self.log.fatal("Could not create profile directory: %r"%location)
326 326 self.exit(1)
327 327 else:
328 328 self.log.debug("Creating new profile dir: %r"%location)
329 329 else:
330 330 self.log.fatal("Profile directory %r not found."%location)
331 331 self.exit(1)
332 332 else:
333 333 self.log.info("Using existing profile dir: %r"%location)
334 334 # if profile_dir is specified explicitly, set profile name
335 335 dir_name = os.path.basename(p.location)
336 336 if dir_name.startswith('profile_'):
337 337 self.profile = dir_name[8:]
338 338
339 339 self.profile_dir = p
340 340 self.config_file_paths.append(p.location)
341 341 self._in_init_profile_dir = False
342 342
343 343 def init_config_files(self):
344 344 """[optionally] copy default config files into profile dir."""
345 345 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS)
346 346 # copy config files
347 347 path = self.builtin_profile_dir
348 348 if self.copy_config_files:
349 349 src = self.profile
350 350
351 351 cfg = self.config_file_name
352 352 if path and os.path.exists(os.path.join(path, cfg)):
353 353 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
354 354 cfg, src, self.profile_dir.location, self.overwrite)
355 355 )
356 356 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
357 357 else:
358 358 self.stage_default_config_file()
359 359 else:
360 360 # Still stage *bundled* config files, but not generated ones
361 361 # This is necessary for `ipython profile=sympy` to load the profile
362 362 # on the first go
363 363 files = glob.glob(os.path.join(path, '*.py'))
364 364 for fullpath in files:
365 365 cfg = os.path.basename(fullpath)
366 366 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
367 367 # file was copied
368 368 self.log.warn("Staging bundled %s from %s into %r"%(
369 369 cfg, self.profile, self.profile_dir.location)
370 370 )
371 371
372 372
373 373 def stage_default_config_file(self):
374 374 """auto generate default config file, and stage it into the profile."""
375 375 s = self.generate_config_file()
376 376 fname = os.path.join(self.profile_dir.location, self.config_file_name)
377 377 if self.overwrite or not os.path.exists(fname):
378 378 self.log.warn("Generating default config file: %r"%(fname))
379 379 with open(fname, 'w') as f:
380 380 f.write(s)
381 381
382 382 @catch_config_error
383 383 def initialize(self, argv=None):
384 384 # don't hook up crash handler before parsing command-line
385 385 self.parse_command_line(argv)
386 386 self.init_crash_handler()
387 387 if self.subapp is not None:
388 388 # stop here if subapp is taking over
389 389 return
390 390 cl_config = self.config
391 391 self.init_profile_dir()
392 392 self.init_config_files()
393 393 self.load_config_file()
394 394 # enforce cl-opts override configfile opts:
395 395 self.update_config(cl_config)
396 396
@@ -1,427 +1,427 b''
1 1 """Utilities for signing notebooks"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 from contextlib import contextmanager
8 8 from datetime import datetime
9 9 import hashlib
10 10 from hmac import HMAC
11 11 import io
12 12 import os
13 13
14 14 try:
15 15 import sqlite3
16 16 except ImportError:
17 17 try:
18 18 from pysqlite2 import dbapi2 as sqlite3
19 19 except ImportError:
20 20 sqlite3 = None
21 21
22 22 from IPython.utils.io import atomic_writing
23 23 from IPython.utils.py3compat import unicode_type, cast_bytes
24 24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
25 25 from IPython.config import LoggingConfigurable, MultipleInstanceError
26 26 from IPython.core.application import BaseIPythonApplication, base_flags
27 27
28 28 from . import read, write, NO_CONVERT
29 29
30 30 try:
31 31 # Python 3
32 32 algorithms = hashlib.algorithms_guaranteed
33 33 except AttributeError:
34 34 algorithms = hashlib.algorithms
35 35
36 36
37 37 def yield_everything(obj):
38 38 """Yield every item in a container as bytes
39 39
40 40 Allows any JSONable object to be passed to an HMAC digester
41 41 without having to serialize the whole thing.
42 42 """
43 43 if isinstance(obj, dict):
44 44 for key in sorted(obj):
45 45 value = obj[key]
46 46 yield cast_bytes(key)
47 47 for b in yield_everything(value):
48 48 yield b
49 49 elif isinstance(obj, (list, tuple)):
50 50 for element in obj:
51 51 for b in yield_everything(element):
52 52 yield b
53 53 elif isinstance(obj, unicode_type):
54 54 yield obj.encode('utf8')
55 55 else:
56 56 yield unicode_type(obj).encode('utf8')
57 57
58 58 def yield_code_cells(nb):
59 59 """Iterator that yields all cells in a notebook
60 60
61 61 nbformat version independent
62 62 """
63 63 if nb.nbformat >= 4:
64 64 for cell in nb['cells']:
65 65 if cell['cell_type'] == 'code':
66 66 yield cell
67 67 elif nb.nbformat == 3:
68 68 for ws in nb['worksheets']:
69 69 for cell in ws['cells']:
70 70 if cell['cell_type'] == 'code':
71 71 yield cell
72 72
73 73 @contextmanager
74 74 def signature_removed(nb):
75 75 """Context manager for operating on a notebook with its signature removed
76 76
77 77 Used for excluding the previous signature when computing a notebook's signature.
78 78 """
79 79 save_signature = nb['metadata'].pop('signature', None)
80 80 try:
81 81 yield
82 82 finally:
83 83 if save_signature is not None:
84 84 nb['metadata']['signature'] = save_signature
85 85
86 86
87 87 class NotebookNotary(LoggingConfigurable):
88 88 """A class for computing and verifying notebook signatures."""
89 89
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir", allow_none=True)
91 91 def _profile_dir_default(self):
92 92 from IPython.core.application import BaseIPythonApplication
93 93 app = None
94 94 try:
95 95 if BaseIPythonApplication.initialized():
96 96 app = BaseIPythonApplication.instance()
97 97 except MultipleInstanceError:
98 98 pass
99 99 if app is None:
100 100 # create an app, without the global instance
101 101 app = BaseIPythonApplication()
102 102 app.initialize(argv=[])
103 103 return app.profile_dir
104 104
105 105 db_file = Unicode(config=True,
106 106 help="""The sqlite file in which to store notebook signatures.
107 107 By default, this will be in your IPython profile.
108 108 You can set it to ':memory:' to disable sqlite writing to the filesystem.
109 109 """)
110 110 def _db_file_default(self):
111 111 if self.profile_dir is None:
112 112 return ':memory:'
113 113 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
114 114
115 115 # 64k entries ~ 12MB
116 116 cache_size = Integer(65535, config=True,
117 117 help="""The number of notebook signatures to cache.
118 118 When the number of signatures exceeds this value,
119 119 the oldest 25% of signatures will be culled.
120 120 """
121 121 )
122 122 db = Any()
123 123 def _db_default(self):
124 124 if sqlite3 is None:
125 125 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
126 126 return
127 127 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
128 128 db = sqlite3.connect(self.db_file, **kwargs)
129 129 self.init_db(db)
130 130 return db
131 131
132 132 def init_db(self, db):
133 133 db.execute("""
134 134 CREATE TABLE IF NOT EXISTS nbsignatures
135 135 (
136 136 id integer PRIMARY KEY AUTOINCREMENT,
137 137 algorithm text,
138 138 signature text,
139 139 path text,
140 140 last_seen timestamp
141 141 )""")
142 142 db.execute("""
143 143 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
144 144 """)
145 145 db.commit()
146 146
147 147 algorithm = Enum(algorithms, default_value='sha256', config=True,
148 148 help="""The hashing algorithm used to sign notebooks."""
149 149 )
150 150 def _algorithm_changed(self, name, old, new):
151 151 self.digestmod = getattr(hashlib, self.algorithm)
152 152
153 153 digestmod = Any()
154 154 def _digestmod_default(self):
155 155 return getattr(hashlib, self.algorithm)
156 156
157 157 secret_file = Unicode(config=True,
158 158 help="""The file where the secret key is stored."""
159 159 )
160 160 def _secret_file_default(self):
161 161 if self.profile_dir is None:
162 162 return ''
163 163 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
164 164
165 165 secret = Bytes(config=True,
166 166 help="""The secret key with which notebooks are signed."""
167 167 )
168 168 def _secret_default(self):
169 169 # note : this assumes an Application is running
170 170 if os.path.exists(self.secret_file):
171 171 with io.open(self.secret_file, 'rb') as f:
172 172 return f.read()
173 173 else:
174 174 secret = base64.encodestring(os.urandom(1024))
175 175 self._write_secret_file(secret)
176 176 return secret
177 177
178 178 def _write_secret_file(self, secret):
179 179 """write my secret to my secret_file"""
180 180 self.log.info("Writing notebook-signing key to %s", self.secret_file)
181 181 with io.open(self.secret_file, 'wb') as f:
182 182 f.write(secret)
183 183 try:
184 184 os.chmod(self.secret_file, 0o600)
185 185 except OSError:
186 186 self.log.warn(
187 187 "Could not set permissions on %s",
188 188 self.secret_file
189 189 )
190 190 return secret
191 191
192 192 def compute_signature(self, nb):
193 193 """Compute a notebook's signature
194 194
195 195 by hashing the entire contents of the notebook via HMAC digest.
196 196 """
197 197 hmac = HMAC(self.secret, digestmod=self.digestmod)
198 198 # don't include the previous hash in the content to hash
199 199 with signature_removed(nb):
200 200 # sign the whole thing
201 201 for b in yield_everything(nb):
202 202 hmac.update(b)
203 203
204 204 return hmac.hexdigest()
205 205
206 206 def check_signature(self, nb):
207 207 """Check a notebook's stored signature
208 208
209 209 If a signature is stored in the notebook's metadata,
210 210 a new signature is computed and compared with the stored value.
211 211
212 212 Returns True if the signature is found and matches, False otherwise.
213 213
214 214 The following conditions must all be met for a notebook to be trusted:
215 215 - a signature is stored in the form 'scheme:hexdigest'
216 216 - the stored scheme matches the requested scheme
217 217 - the requested scheme is available from hashlib
218 218 - the computed hash from notebook_signature matches the stored hash
219 219 """
220 220 if nb.nbformat < 3:
221 221 return False
222 222 if self.db is None:
223 223 return False
224 224 signature = self.compute_signature(nb)
225 225 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
226 226 algorithm = ? AND
227 227 signature = ?;
228 228 """, (self.algorithm, signature)).fetchone()
229 229 if r is None:
230 230 return False
231 231 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
232 232 algorithm = ? AND
233 233 signature = ?;
234 234 """,
235 235 (datetime.utcnow(), self.algorithm, signature),
236 236 )
237 237 self.db.commit()
238 238 return True
239 239
240 240 def sign(self, nb):
241 241 """Sign a notebook, indicating that its output is trusted on this machine
242 242
243 243 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
244 244 """
245 245 if nb.nbformat < 3:
246 246 return
247 247 signature = self.compute_signature(nb)
248 248 self.store_signature(signature, nb)
249 249
250 250 def store_signature(self, signature, nb):
251 251 if self.db is None:
252 252 return
253 253 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
254 254 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
255 255 (self.algorithm, signature, datetime.utcnow())
256 256 )
257 257 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
258 258 algorithm = ? AND
259 259 signature = ?;
260 260 """,
261 261 (datetime.utcnow(), self.algorithm, signature),
262 262 )
263 263 self.db.commit()
264 264 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
265 265 if n > self.cache_size:
266 266 self.cull_db()
267 267
268 268 def unsign(self, nb):
269 269 """Ensure that a notebook is untrusted
270 270
271 271 by removing its signature from the trusted database, if present.
272 272 """
273 273 signature = self.compute_signature(nb)
274 274 self.db.execute("""DELETE FROM nbsignatures WHERE
275 275 algorithm = ? AND
276 276 signature = ?;
277 277 """,
278 278 (self.algorithm, signature)
279 279 )
280 280 self.db.commit()
281 281
282 282 def cull_db(self):
283 283 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
284 284 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
285 285 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
286 286 );
287 287 """, (max(int(0.75 * self.cache_size), 1),))
288 288
289 289 def mark_cells(self, nb, trusted):
290 290 """Mark cells as trusted if the notebook's signature can be verified
291 291
292 292 Sets ``cell.metadata.trusted = True | False`` on all code cells,
293 293 depending on whether the stored signature can be verified.
294 294
295 295 This function is the inverse of check_cells
296 296 """
297 297 if nb.nbformat < 3:
298 298 return
299 299
300 300 for cell in yield_code_cells(nb):
301 301 cell['metadata']['trusted'] = trusted
302 302
303 303 def _check_cell(self, cell, nbformat_version):
304 304 """Do we trust an individual cell?
305 305
306 306 Return True if:
307 307
308 308 - cell is explicitly trusted
309 309 - cell has no potentially unsafe rich output
310 310
311 311 If a cell has no output, or only simple print statements,
312 312 it will always be trusted.
313 313 """
314 314 # explicitly trusted
315 315 if cell['metadata'].pop("trusted", False):
316 316 return True
317 317
318 318 # explicitly safe output
319 319 if nbformat_version >= 4:
320 320 unsafe_output_types = ['execute_result', 'display_data']
321 321 safe_keys = {"output_type", "execution_count", "metadata"}
322 322 else: # v3
323 323 unsafe_output_types = ['pyout', 'display_data']
324 324 safe_keys = {"output_type", "prompt_number", "metadata"}
325 325
326 326 for output in cell['outputs']:
327 327 output_type = output['output_type']
328 328 if output_type in unsafe_output_types:
329 329 # if there are any data keys not in the safe whitelist
330 330 output_keys = set(output)
331 331 if output_keys.difference(safe_keys):
332 332 return False
333 333
334 334 return True
335 335
336 336 def check_cells(self, nb):
337 337 """Return whether all code cells are trusted
338 338
339 339 If there are no code cells, return True.
340 340
341 341 This function is the inverse of mark_cells.
342 342 """
343 343 if nb.nbformat < 3:
344 344 return False
345 345 trusted = True
346 346 for cell in yield_code_cells(nb):
347 347 # only distrust a cell if it actually has some output to distrust
348 348 if not self._check_cell(cell, nb.nbformat):
349 349 trusted = False
350 350
351 351 return trusted
352 352
353 353
354 354 trust_flags = {
355 355 'reset' : (
356 356 {'TrustNotebookApp' : { 'reset' : True}},
357 357 """Delete the trusted notebook cache.
358 358 All previously signed notebooks will become untrusted.
359 359 """
360 360 ),
361 361 }
362 362 trust_flags.update(base_flags)
363 363 trust_flags.pop('init')
364 364
365 365
366 366 class TrustNotebookApp(BaseIPythonApplication):
367 367
368 368 description="""Sign one or more IPython notebooks with your key,
369 369 to trust their dynamic (HTML, Javascript) output.
370 370
371 371 Trusting a notebook only applies to the current IPython profile.
372 372 To trust a notebook for use with a profile other than default,
373 373 add `--profile [profile name]`.
374 374
375 375 Otherwise, you will have to re-execute the notebook to see output.
376 376 """
377 377
378 378 examples = """
379 379 ipython trust mynotebook.ipynb and_this_one.ipynb
380 380 ipython trust --profile myprofile mynotebook.ipynb
381 381 """
382 382
383 383 flags = trust_flags
384 384
385 385 reset = Bool(False, config=True,
386 386 help="""If True, delete the trusted signature cache.
387 387 After reset, all previously signed notebooks will become untrusted.
388 388 """
389 389 )
390 390
391 391 notary = Instance(NotebookNotary)
392 392 def _notary_default(self):
393 393 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
394 394
395 395 def sign_notebook(self, notebook_path):
396 396 if not os.path.exists(notebook_path):
397 397 self.log.error("Notebook missing: %s" % notebook_path)
398 398 self.exit(1)
399 399 with io.open(notebook_path, encoding='utf8') as f:
400 400 nb = read(f, NO_CONVERT)
401 401 if self.notary.check_signature(nb):
402 402 print("Notebook already signed: %s" % notebook_path)
403 403 else:
404 404 print("Signing notebook: %s" % notebook_path)
405 405 self.notary.sign(nb)
406 406 with atomic_writing(notebook_path) as f:
407 407 write(nb, f, NO_CONVERT)
408 408
409 409 def generate_new_key(self):
410 410 """Generate a new notebook signature key"""
411 411 print("Generating new notebook key: %s" % self.notary.secret_file)
412 412 self.notary._write_secret_file(os.urandom(1024))
413 413
414 414 def start(self):
415 415 if self.reset:
416 416 if os.path.exists(self.notary.db_file):
417 417 print("Removing trusted signature cache: %s" % self.notary.db_file)
418 418 os.remove(self.notary.db_file)
419 419 self.generate_new_key()
420 420 return
421 421 if not self.extra_args:
422 422 self.log.critical("Specify at least one notebook to sign.")
423 423 self.exit(1)
424 424
425 425 for notebook_path in self.extra_args:
426 426 self.sign_notebook(notebook_path)
427 427
General Comments 0
You need to be logged in to leave comments. Login now