Skip to content

Commit f5f97d1

Browse files
authored
Merge pull request #2007 from minrk/password
add `jupyter notebook password` entrypoint
2 parents 2659ff8 + f7b85b0 commit f5f97d1

File tree

5 files changed

+119
-5
lines changed

5 files changed

+119
-5
lines changed

docs/source/public_server.rst

+15-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ configuring the :attr:`NotebookApp.password` setting in
5353

5454
Prerequisite: A notebook configuration file
5555
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
56+
5657
Check to see if you have a notebook configuration file,
5758
:file:`jupyter_notebook_config.py`. The default location for this file
5859
is your Jupyter folder in your home directory, ``~/.jupyter``.
@@ -66,7 +67,20 @@ using the following command::
6667

6768
Preparing a hashed password
6869
~~~~~~~~~~~~~~~~~~~~~~~~~~~
69-
You can prepare a hashed password using the function
70+
71+
As of notebook version 5.0, you can enter and store a password for your
72+
notebook server with a single command.
73+
:command:`jupyter notebook password` will prompt you for your password
74+
and record the hashed password in your :file:`jupyter_notebook_config.json`.
75+
76+
.. code-block:: bash
77+
78+
$ jupyter notebook password
79+
Enter password: ****
80+
Verify password: ****
81+
[NotebookPasswordApp] Wrote hashed password to /Users/you/.jupyter/jupyter_notebook_config.json
82+
83+
You can prepare a hashed password manually, using the function
7084
:func:`notebook.auth.security.passwd`:
7185

7286
.. code-block:: ipython

docs/source/security.rst

+13-1
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,20 @@ Once you have visited this URL,
5858
a cookie will be set in your browser and you won't need to use the token again,
5959
unless you switch browsers, clear your cookies, or start a notebook server on a new port.
6060

61+
Alternatives to token authentication
62+
------------------------------------
6163

62-
You can disable authentication altogether by setting the token and password to empty strings,
64+
If a generated token doesn't work well for you,
65+
you can set a password for your notebook.
66+
:command:`jupyter notebook password` will prompt you for a password,
67+
and store the hashed password in your :file:`jupyter_notebook_config.json`.
68+
69+
.. versionadded:: 5.0
70+
71+
:command:`jupyter notebook password` command is added.
72+
73+
74+
It is possible disable authentication altogether by setting the token and password to empty strings,
6375
but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application:
6476

6577
.. sourcecode:: python

notebook/auth/security.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
"""
22
Password generation for the Notebook.
33
"""
4+
5+
from contextlib import contextmanager
46
import getpass
57
import hashlib
8+
import io
9+
import json
10+
import os
611
import random
12+
import traceback
13+
import warnings
714

8-
from ipython_genutils.py3compat import cast_bytes, str_to_bytes
15+
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
16+
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
17+
from jupyter_core.paths import jupyter_config_dir
918

1019
# Length of the salt in nr of hex chars, which implies salt_len * 4
1120
# bits of randomness.
@@ -99,3 +108,41 @@ def passwd_check(hashed_passphrase, passphrase):
99108
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))
100109

101110
return h.hexdigest() == pw_digest
111+
112+
@contextmanager
113+
def persist_config(config_file=None, mode=0o600):
114+
"""Context manager that can be used to modify a config object
115+
116+
On exit of the context manager, the config will be written back to disk,
117+
by default with user-only (600) permissions.
118+
"""
119+
120+
if config_file is None:
121+
config_file = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json')
122+
123+
loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file))
124+
try:
125+
config = loader.load_config()
126+
except ConfigFileNotFound:
127+
config = Config()
128+
129+
yield config
130+
131+
with io.open(config_file, 'w', encoding='utf8') as f:
132+
f.write(cast_unicode(json.dumps(config, indent=2)))
133+
134+
try:
135+
os.chmod(config_file, mode)
136+
except Exception as e:
137+
tb = traceback.format_exc()
138+
warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb),
139+
RuntimeWarning)
140+
141+
142+
def set_password(password=None, config_file=None):
143+
"""Ask user for password, store it in notebook json configuration file"""
144+
145+
hashed_password = passwd(password)
146+
147+
with persist_config(config_file) as config:
148+
config.NotebookApp.password = hashed_password

notebook/notebookapp.py

+20
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
_examples = """
107107
jupyter notebook # start the notebook
108108
jupyter notebook --certfile=mycert.pem # use SSL/TLS certificate
109+
jupyter notebook password # enter a password to protect the server
109110
"""
110111

111112
DEV_NOTE_NPM = """It looks like you're running the notebook from source.
@@ -325,6 +326,24 @@ def init_handlers(self, settings):
325326
return new_handlers
326327

327328

329+
class NotebookPasswordApp(JupyterApp):
330+
"""Set a password for the notebook server.
331+
332+
Setting a password secures the notebook server
333+
and removes the need for token-based authentication.
334+
"""
335+
336+
description = __doc__
337+
338+
def _config_file_default(self):
339+
return os.path.join(self.config_dir, 'jupyter_notebook_config.json')
340+
341+
def start(self):
342+
from .auth.security import set_password
343+
set_password(config_file=self.config_file)
344+
self.log.info("Wrote hashed password to %s" % self.config_file)
345+
346+
328347
class NbserverListApp(JupyterApp):
329348
version = __version__
330349
description="List currently running notebook servers."
@@ -428,6 +447,7 @@ class NotebookApp(JupyterApp):
428447

429448
subcommands = dict(
430449
list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
450+
password=(NotebookPasswordApp, NotebookPasswordApp.description.splitlines()[0]),
431451
)
432452

433453
_log_formatter_cls = LogFormatter

notebook/tests/test_notebookapp.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
"""Test NotebookApp"""
22

3-
3+
import getpass
44
import logging
55
import os
66
import re
7+
from subprocess import Popen, PIPE, STDOUT
8+
import sys
79
from tempfile import NamedTemporaryFile
810

11+
try:
12+
from unittest.mock import patch
13+
except ImportError:
14+
from mock import patch # py2
15+
916
import nose.tools as nt
1017

1118
from traitlets.tests.utils import check_help_all_output
@@ -14,7 +21,7 @@
1421
from ipython_genutils.tempdir import TemporaryDirectory
1522
from traitlets import TraitError
1623
from notebook import notebookapp, __version__
17-
from notebook import notebookapp
24+
from notebook.auth.security import passwd_check
1825
NotebookApp = notebookapp.NotebookApp
1926

2027

@@ -117,3 +124,17 @@ def raise_on_bad_version(version):
117124

118125
def test_current_version():
119126
raise_on_bad_version(__version__)
127+
128+
def test_notebook_password():
129+
password = 'secret'
130+
with TemporaryDirectory() as td:
131+
with patch.dict('os.environ', {
132+
'JUPYTER_CONFIG_DIR': td,
133+
}), patch.object(getpass, 'getpass', return_value=password):
134+
app = notebookapp.NotebookPasswordApp(log_level=logging.ERROR)
135+
app.initialize([])
136+
app.start()
137+
nb = NotebookApp()
138+
nb.load_config_file()
139+
nt.assert_not_equal(nb.password, '')
140+
passwd_check(nb.password, password)

0 commit comments

Comments
 (0)