From cd7e0a939c5c7fc4bf7243b010ff87d7d0777b6a Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 22 Dec 2016 13:43:05 +0100 Subject: [PATCH 1/4] add `jupyter notebook password` entrypoint records, hashes, and stores password in json config --- notebook/auth/security.py | 48 ++++++++++++++++++++++++++++++++++++++- notebook/notebookapp.py | 18 +++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/notebook/auth/security.py b/notebook/auth/security.py index b9865a3ca..7441d5409 100644 --- a/notebook/auth/security.py +++ b/notebook/auth/security.py @@ -1,11 +1,20 @@ """ Password generation for the Notebook. """ + +from contextlib import contextmanager import getpass import hashlib +import io +import json +import os import random +import sys +import traceback -from ipython_genutils.py3compat import cast_bytes, str_to_bytes +from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode +from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader +from jupyter_core.paths import jupyter_config_dir # Length of the salt in nr of hex chars, which implies salt_len * 4 # bits of randomness. @@ -99,3 +108,40 @@ def passwd_check(hashed_passphrase, passphrase): h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii')) return h.hexdigest() == pw_digest + +@contextmanager +def persist_config(config_file=None, mode=0o600): + """Context manager that can be used to modify a config object + + On exit of the context manager, the config will be written back to disk, + by default with user-only (600) permissions. + """ + + if config_file is None: + config_file = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json') + + loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file)) + try: + config = loader.load_config() + except ConfigFileNotFound: + config = Config() + + yield config + + with io.open(config_file, 'w', encoding='utf8') as f: + f.write(cast_unicode(json.dumps(config, indent=2))) + + try: + os.chmod(config_file, mode) + except Exception: + print("Failed to set permissions on %s:" % config_file, file=sys.stderr) + traceback.print_exc(file=sys.stderr) + + +def set_password(password=None, config_file=None): + """Ask user for password, store it in notebook json configuration file""" + + hashed_password = passwd(password) + + with persist_config(config_file) as config: + config.NotebookApp.password = hashed_password diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 7998c14d4..82b68c5dd 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -106,6 +106,7 @@ from .utils import url_path_join, check_pid, url_escape _examples = """ jupyter notebook # start the notebook jupyter notebook --certfile=mycert.pem # use SSL/TLS certificate +jupyter notebook password # enter a password to protect the server """ DEV_NOTE_NPM = """It looks like you're running the notebook from source. @@ -323,6 +324,22 @@ class NotebookWebApplication(web.Application): return new_handlers +class NotebookPasswordApp(JupyterApp): + """Set a password for the notebook server. + + Setting a password secures the notebook server + and removes the need for token-based authentication. + """ + + def _config_file_default(self): + return os.path.join(self.config_dir, 'jupyter_notebook_config.json') + description = __doc__ + def start(self): + from .auth.security import set_password + set_password(config_file=self.config_file) + self.log.info("Wrote hashed password to %s" % self.config_file) + + class NbserverListApp(JupyterApp): version = __version__ description="List currently running notebook servers." @@ -426,6 +443,7 @@ class NotebookApp(JupyterApp): subcommands = dict( list=(NbserverListApp, NbserverListApp.description.splitlines()[0]), + password=(NotebookPasswordApp, NotebookPasswordApp.description.splitlines()[0]), ) _log_formatter_cls = LogFormatter From 53f809d40742935ca856a2cf1f6d0acd4989bb39 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 3 Jan 2017 14:50:56 +0100 Subject: [PATCH 2/4] use a warning for failure to set file permissions instead of print --- notebook/auth/security.py | 9 +++++---- notebook/notebookapp.py | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/notebook/auth/security.py b/notebook/auth/security.py index 7441d5409..8f69148ee 100644 --- a/notebook/auth/security.py +++ b/notebook/auth/security.py @@ -9,8 +9,8 @@ import io import json import os import random -import sys import traceback +import warnings from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader @@ -133,9 +133,10 @@ def persist_config(config_file=None, mode=0o600): try: os.chmod(config_file, mode) - except Exception: - print("Failed to set permissions on %s:" % config_file, file=sys.stderr) - traceback.print_exc(file=sys.stderr) + except Exception as e: + tb = traceback.format_exc() + warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb), + RuntimeWarning) def set_password(password=None, config_file=None): diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 82b68c5dd..ed14daa5c 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -331,9 +331,11 @@ class NotebookPasswordApp(JupyterApp): and removes the need for token-based authentication. """ + description = __doc__ + def _config_file_default(self): return os.path.join(self.config_dir, 'jupyter_notebook_config.json') - description = __doc__ + def start(self): from .auth.security import set_password set_password(config_file=self.config_file) From f871d64e6e1cc72533d15884a0e6f5dafc21ace4 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 3 Jan 2017 15:15:44 +0100 Subject: [PATCH 3/4] exercise password app --- notebook/tests/test_notebookapp.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/notebook/tests/test_notebookapp.py b/notebook/tests/test_notebookapp.py index 22a9cfc12..a7f38e2cd 100644 --- a/notebook/tests/test_notebookapp.py +++ b/notebook/tests/test_notebookapp.py @@ -1,11 +1,18 @@ """Test NotebookApp""" - +import getpass import logging import os import re +from subprocess import Popen, PIPE, STDOUT +import sys from tempfile import NamedTemporaryFile +try: + from unittest.mock import patch +except ImportError: + from mock import patch # py2 + import nose.tools as nt from traitlets.tests.utils import check_help_all_output @@ -14,7 +21,7 @@ from jupyter_core.application import NoStart from ipython_genutils.tempdir import TemporaryDirectory from traitlets import TraitError from notebook import notebookapp, __version__ -from notebook import notebookapp +from notebook.auth.security import passwd_check NotebookApp = notebookapp.NotebookApp @@ -117,3 +124,17 @@ def raise_on_bad_version(version): def test_current_version(): raise_on_bad_version(__version__) + +def test_notebook_password(): + password = 'secret' + with TemporaryDirectory() as td: + with patch.dict('os.environ', { + 'JUPYTER_CONFIG_DIR': td, + }), patch.object(getpass, 'getpass', return_value=password): + app = notebookapp.NotebookPasswordApp(log_level=logging.ERROR) + app.initialize([]) + app.start() + nb = NotebookApp() + nb.load_config_file() + nt.assert_not_equal(nb.password, '') + passwd_check(nb.password, password) From f7b85b099994c4932ec102be890fdfc008e8888d Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 3 Jan 2017 15:27:02 +0100 Subject: [PATCH 4/4] include jupyter notebook password entrypoint in secure notebook docs --- docs/source/public_server.rst | 16 +++++++++++++++- docs/source/security.rst | 14 +++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/source/public_server.rst b/docs/source/public_server.rst index 7b8ce216a..efe5a86e3 100644 --- a/docs/source/public_server.rst +++ b/docs/source/public_server.rst @@ -53,6 +53,7 @@ configuring the :attr:`NotebookApp.password` setting in Prerequisite: A notebook configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Check to see if you have a notebook configuration file, :file:`jupyter_notebook_config.py`. The default location for this file is your Jupyter folder in your home directory, ``~/.jupyter``. @@ -66,7 +67,20 @@ using the following command:: Preparing a hashed password ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can prepare a hashed password using the function + +As of notebook version 5.0, you can enter and store a password for your +notebook server with a single command. +:command:`jupyter notebook password` will prompt you for your password +and record the hashed password in your :file:`jupyter_notebook_config.json`. + +.. code-block:: bash + + $ jupyter notebook password + Enter password: **** + Verify password: **** + [NotebookPasswordApp] Wrote hashed password to /Users/you/.jupyter/jupyter_notebook_config.json + +You can prepare a hashed password manually, using the function :func:`notebook.auth.security.passwd`: .. code-block:: ipython diff --git a/docs/source/security.rst b/docs/source/security.rst index 491270789..aaa8dfce3 100644 --- a/docs/source/security.rst +++ b/docs/source/security.rst @@ -58,8 +58,20 @@ Once you have visited this URL, a cookie will be set in your browser and you won't need to use the token again, unless you switch browsers, clear your cookies, or start a notebook server on a new port. +Alternatives to token authentication +------------------------------------ -You can disable authentication altogether by setting the token and password to empty strings, +If a generated token doesn't work well for you, +you can set a password for your notebook. +:command:`jupyter notebook password` will prompt you for a password, +and store the hashed password in your :file:`jupyter_notebook_config.json`. + +.. versionadded:: 5.0 + + :command:`jupyter notebook password` command is added. + + +It is possible disable authentication altogether by setting the token and password to empty strings, but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application: .. sourcecode:: python