From a3e3f24d2d24e2ce1a6f1b5c49fca9f8936fd3b9 Mon Sep 17 00:00:00 2001 From: John Paton Date: Fri, 14 Feb 2020 16:35:48 +0100 Subject: [PATCH 01/20] Add favicon to the base page template This was missing before. Giving it its own named block will let users customize it if they wish. --- share/jupyterhub/templates/page.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 50f910c5..b40be095 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -34,6 +34,9 @@ {% block stylesheet %} {% endblock %} + {% block favicon %} + + {% endblock %} {% block scripts %} From a999ac8f073e2ff6b6e084a05eb40dd3993292c8 Mon Sep 17 00:00:00 2001 From: JohnPaton Date: Fri, 14 Feb 2020 16:51:27 +0100 Subject: [PATCH 02/20] Use only rel="icon" This is the officially recommended method from MDN https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/The_head_metadata_in_HTML --- share/jupyterhub/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index b40be095..bc701ce4 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -35,7 +35,7 @@ {% endblock %} {% block favicon %} - + {% endblock %} {% block scripts %} From 79a51dfdce028e83d2921fdf29053307bdeb896d Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 18 Feb 2020 17:10:19 +0100 Subject: [PATCH 03/20] make init_spawners check O(running servers) not O(total users) query on Server objects instead of User objects avoids lots of ORM work on startup since there are typically a small number of running servers relative to the total number of users this also means that the users dict is not fully populated. Is that okay? I hope so. --- jupyterhub/app.py | 39 ++++++++++++++++++++++++--------------- jupyterhub/orm.py | 4 ++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 0bd57de7..b0cc694a 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1670,9 +1670,12 @@ class JupyterHub(Application): # This lets whitelist be used to set up initial list, # but changes to the whitelist can occur in the database, # and persist across sessions. + total_users = 0 for user in db.query(orm.User): try: - await maybe_future(self.authenticator.add_user(user)) + f = self.authenticator.add_user(user) + if f: + await maybe_future() except Exception: self.log.exception("Error adding user %s already in db", user.name) if self.authenticator.delete_invalid_users: @@ -1694,6 +1697,7 @@ class JupyterHub(Application): ) ) else: + total_users += 1 # handle database upgrades where user.created is undefined. # we don't want to allow user.created to be undefined, # so initialize it to last_activity (if defined) or now. @@ -1705,6 +1709,8 @@ class JupyterHub(Application): # From this point on, any user changes should be done simultaneously # to the whitelist set and user db, unless the whitelist is empty (all users allowed). + TOTAL_USERS.set(total_users) + async def init_groups(self): """Load predefined groups into the database""" db = self.db @@ -2005,21 +2011,24 @@ class JupyterHub(Application): spawner._check_pending = False # parallelize checks for running Spawners + # run query on extant Server objects + # so this is O(running servers) not O(total users) check_futures = [] - for orm_user in db.query(orm.User): - user = self.users[orm_user] - self.log.debug("Loading state for %s from db", user.name) - for name, orm_spawner in user.orm_spawners.items(): - if orm_spawner.server is not None: - # spawner should be running - # instantiate Spawner wrapper and check if it's still alive - spawner = user.spawners[name] - # signal that check is pending to avoid race conditions - spawner._check_pending = True - f = asyncio.ensure_future(check_spawner(user, name, spawner)) - check_futures.append(f) - - TOTAL_USERS.set(len(self.users)) + for orm_server in db.query(orm.Server): + orm_spawners = orm_server.spawner + if not orm_spawners: + continue + orm_spawner = orm_spawners[0] + orm_user = orm_spawner.user + # instantiate Spawner wrapper and check if it's still alive + # spawner should be running + user = self.users[orm_spawner.user] + spawner = user.spawners[orm_spawner.name] + self.log.debug("Loading state for %s from db", spawner._log_name) + # signal that check is pending to avoid race conditions + spawner._check_pending = True + f = asyncio.ensure_future(check_spawner(user, spawner.name, spawner)) + check_futures.append(f) # it's important that we get here before the first await # so that we know all spawners are instantiated and in the check-pending state diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 3198a046..4e151092 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -230,7 +230,7 @@ class Spawner(Base): user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE')) server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL')) - server = relationship(Server, cascade="all") + server = relationship(Server, backref='spawner', cascade="all") state = Column(JSONDict) name = Column(Unicode(255)) @@ -282,7 +282,7 @@ class Service(Base): # service-specific interface _server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL')) - server = relationship(Server, cascade='all') + server = relationship(Server, backref='service', cascade='all') pid = Column(Integer) def new_api_token(self, token=None, **kwargs): From 13313abb37325aacb2f89f0ce8c3ea61fb53430b Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 19 Feb 2020 10:46:49 +1300 Subject: [PATCH 04/20] Fix link to SSL encryption from troubleshooting page --- docs/source/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/troubleshooting.md b/docs/source/troubleshooting.md index 617a5f44..dc222942 100644 --- a/docs/source/troubleshooting.md +++ b/docs/source/troubleshooting.md @@ -269,7 +269,7 @@ where `ssl_cert` is example-chained.crt and ssl_key to your private key. Then restart JupyterHub. -See also [JupyterHub SSL encryption](getting-started.md#ssl-encryption). +See also [JupyterHub SSL encryption](./getting-started/security-basics.html#ssl-encryption). ### Install JupyterHub without a network connection From dec324475899767babb4c82199e4b7cc1a35b2cf Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 19 Feb 2020 12:39:20 +1300 Subject: [PATCH 05/20] Use fixed commit plus line number in github link --- docs/source/reference/services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/services.md b/docs/source/reference/services.md index 7686d4a4..ec446c8e 100644 --- a/docs/source/reference/services.md +++ b/docs/source/reference/services.md @@ -360,7 +360,7 @@ and taking note of the following process: An example of using an Externally-Managed Service and authentication is in [nbviewer README][nbviewer example] section on securing the notebook viewer, -and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94). +and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/ed942b10a52b6259099e2dd687930871dc8aac22/nbviewer/providers/base.py#L95). nbviewer can also be run as a Hub-Managed Service as described [nbviewer README][nbviewer example] section on securing the notebook viewer. From 31c0788bd964fff6178e4a88e7e1f955071a0407 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 19 Feb 2020 12:56:02 +1300 Subject: [PATCH 06/20] Move cookies to the end of the list (ssl, proxy, and then cookies) --- .../getting-started/security-basics.rst | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 80996555..7661cfd1 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -80,6 +80,44 @@ To achieve this, simply omit the configuration settings ``c.JupyterHub.ssl_key`` and ``c.JupyterHub.ssl_cert`` (setting them to ``None`` does not have the same effect, and is an error). +.. _authentication-token: + +Proxy authentication token +-------------------------- + +The Hub authenticates its requests to the Proxy using a secret token that +the Hub and Proxy agree upon. The value of this string should be a random +string (for example, generated by ``openssl rand -hex 32``). + +Generating and storing token in the configuration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Or you can set the value in the configuration file, ``jupyterhub_config.py``: + +.. code-block:: python + + c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5' + +Generating and storing as an environment variable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can pass this value of the proxy authentication token to the Hub and Proxy +using the ``CONFIGPROXY_AUTH_TOKEN`` environment variable: + +.. code-block:: bash + + export CONFIGPROXY_AUTH_TOKEN=$(openssl rand -hex 32) + +This environment variable needs to be visible to the Hub and Proxy. + +Default if token is not set +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you don't set the Proxy authentication token, the Hub will generate a random +key itself, which means that any time you restart the Hub you **must also +restart the Proxy**. If the proxy is a subprocess of the Hub, this should happen +automatically (this is the default configuration). + .. _cookie-secret: Cookie secret @@ -145,42 +183,3 @@ itself, ``jupyterhub_config.py``, as a binary string: If the cookie secret value changes for the Hub, all single-user notebook servers must also be restarted. - - -.. _authentication-token: - -Proxy authentication token --------------------------- - -The Hub authenticates its requests to the Proxy using a secret token that -the Hub and Proxy agree upon. The value of this string should be a random -string (for example, generated by ``openssl rand -hex 32``). - -Generating and storing token in the configuration file -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Or you can set the value in the configuration file, ``jupyterhub_config.py``: - -.. code-block:: python - - c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5' - -Generating and storing as an environment variable -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can pass this value of the proxy authentication token to the Hub and Proxy -using the ``CONFIGPROXY_AUTH_TOKEN`` environment variable: - -.. code-block:: bash - - export CONFIGPROXY_AUTH_TOKEN=$(openssl rand -hex 32) - -This environment variable needs to be visible to the Hub and Proxy. - -Default if token is not set -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't set the Proxy authentication token, the Hub will generate a random -key itself, which means that any time you restart the Hub you **must also -restart the Proxy**. If the proxy is a subprocess of the Hub, this should happen -automatically (this is the default configuration). From b92efcd7b064315e63219a4ef807c40cf541484e Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 09:37:08 +0100 Subject: [PATCH 07/20] spawner test assumed app.users is populated --- jupyterhub/tests/test_spawner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jupyterhub/tests/test_spawner.py b/jupyterhub/tests/test_spawner.py index 177c1da0..fa0cbeab 100644 --- a/jupyterhub/tests/test_spawner.py +++ b/jupyterhub/tests/test_spawner.py @@ -103,7 +103,8 @@ async def wait_for_spawner(spawner, timeout=10): async def test_single_user_spawner(app, request): - user = next(iter(app.users.values()), None) + orm_user = app.db.query(orm.User).first() + user = app.users[orm_user] spawner = user.spawner spawner.cmd = ['jupyterhub-singleuser'] await user.spawn() From a0b6d8ec6f60877d3c90d71e564c5ef3de6e9e89 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 12:12:55 +0100 Subject: [PATCH 08/20] add allow_implicit_spawn setting - warn that there are known issues associated with enabling it - it is inherently incompatible with named servers --- jupyterhub/app.py | 28 ++++++++++++++++++++++++++++ jupyterhub/handlers/base.py | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 0bd57de7..9df85c66 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -917,10 +917,37 @@ class JupyterHub(Application): def _authenticator_default(self): return self.authenticator_class(parent=self, db=self.db) + allow_implicit_spawn = Bool( + False, + help="""Allow implicit spawning + + When a user visits a URL for a server that's not running, + instead of confirming the spawn request, + automatically begin the spawn process. + + Warning: not compatible with named servers, + and known to cause issues with redirect loops, + server errors, and infinitely respawning servers. + """, + ).tag(config=True) + + @validate('allow_implicit_spawn') + def _allow_named_changed(self, proposal): + if proposal.value and self.allow_named_servers: + self.log.warning("Implicit spawn cannot work with named servers") + return False + return proposal.value + allow_named_servers = Bool( False, help="Allow named single-user servers per user" ).tag(config=True) + @observe('allow_named_servers') + def _allow_named_changed(self, change): + if change.new and self.allow_implicit_spawn: + self.log.warning("Implicit spawn cannot work with named servers") + self.allow_implicit_spawn = False + named_server_limit_per_user = Integer( 0, help=""" @@ -2158,6 +2185,7 @@ class JupyterHub(Application): subdomain_host=self.subdomain_host, domain=self.domain, statsd=self.statsd, + allow_implicit_spawn=self.allow_implicit_spawn, allow_named_servers=self.allow_named_servers, default_server_name=self._default_server_name, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 77c2ae27..9910cdde 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1426,11 +1426,17 @@ class UserUrlHandler(BaseHandler): # serve a page prompting for spawn and 503 error # visiting /user/:name no longer triggers implicit spawn # without explicit user action - self.set_status(503) spawn_url = url_concat( url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name), {"next": self.request.uri}, ) + if self.settings["allow_implicit_spawn"]: + self.log.warning("Allowing implicit spawn for %s", self.request.path) + self.redirect(spawn_url) + return + else: + self.set_status(503) + auth_state = await user.get_auth_state() html = self.render_template( "not_running.html", From 436757dd5547319b409e13756c5103a331c8abcb Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 12:43:39 +0100 Subject: [PATCH 09/20] handle implicit spawn with a javascript redirect less dangerous than using a Location redirect, so remove conflicts delay is a user-configurable timer (0 = no implicit spawn, default) --- jupyterhub/app.py | 36 ++++++++------------- jupyterhub/handlers/base.py | 8 ++--- share/jupyterhub/templates/not_running.html | 24 +++++++++++++- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 9df85c66..ee52bd9a 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -917,37 +917,29 @@ class JupyterHub(Application): def _authenticator_default(self): return self.authenticator_class(parent=self, db=self.db) - allow_implicit_spawn = Bool( - False, - help="""Allow implicit spawning + implicit_spawn_seconds = Float( + 0, + help="""Trigger implicit spawns after this many seconds. When a user visits a URL for a server that's not running, - instead of confirming the spawn request, - automatically begin the spawn process. + they are shown a page indicating that the requested server + is not running with a button to spawn the server. - Warning: not compatible with named servers, - and known to cause issues with redirect loops, - server errors, and infinitely respawning servers. + Setting this to a positive value will redirect the user + after this many seconds, effectively clicking this button + automatically for the users, + automatically beginning the spawn process. + + Warning: this can result in errors and surprising behavior + when sharing access URLs to actual servers, + since the wrong server is likely to be started. """, ).tag(config=True) - @validate('allow_implicit_spawn') - def _allow_named_changed(self, proposal): - if proposal.value and self.allow_named_servers: - self.log.warning("Implicit spawn cannot work with named servers") - return False - return proposal.value - allow_named_servers = Bool( False, help="Allow named single-user servers per user" ).tag(config=True) - @observe('allow_named_servers') - def _allow_named_changed(self, change): - if change.new and self.allow_implicit_spawn: - self.log.warning("Implicit spawn cannot work with named servers") - self.allow_implicit_spawn = False - named_server_limit_per_user = Integer( 0, help=""" @@ -2185,7 +2177,7 @@ class JupyterHub(Application): subdomain_host=self.subdomain_host, domain=self.domain, statsd=self.statsd, - allow_implicit_spawn=self.allow_implicit_spawn, + implicit_spawn_seconds=self.implicit_spawn_seconds, allow_named_servers=self.allow_named_servers, default_server_name=self._default_server_name, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 9910cdde..9606fa61 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1430,12 +1430,7 @@ class UserUrlHandler(BaseHandler): url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name), {"next": self.request.uri}, ) - if self.settings["allow_implicit_spawn"]: - self.log.warning("Allowing implicit spawn for %s", self.request.path) - self.redirect(spawn_url) - return - else: - self.set_status(503) + self.set_status(503) auth_state = await user.get_auth_state() html = self.render_template( @@ -1444,6 +1439,7 @@ class UserUrlHandler(BaseHandler): server_name=server_name, spawn_url=spawn_url, auth_state=auth_state, + implicit_spawn_seconds=self.settings.get("implicit_spawn_seconds", 0), ) self.finish(html) diff --git a/share/jupyterhub/templates/not_running.html b/share/jupyterhub/templates/not_running.html index 182e7ba0..fa493d97 100644 --- a/share/jupyterhub/templates/not_running.html +++ b/share/jupyterhub/templates/not_running.html @@ -23,7 +23,14 @@ {% endif %} Would you like to retry starting it? {% else %} - Your server {{ server_name }} is not running. Would you like to start it? + Your server {{ server_name }} is not running. + {% if implicit_spawn_seconds %} + It will be restarted automatically. + If you are not redirected in a few seconds, + click below to launch your server. + {% else %} + Would you like to start it? + {% endif %} {% endif %}

{% endblock %} @@ -42,3 +49,18 @@ {% endblock %} +{% block script %} +{{ super () }} +{% if implicit_spawn_seconds %} + +{% endif %} +{% endblock script %} From 0787489e1bbac11d14482a96c1179755a210d2ba Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 12:53:15 +0100 Subject: [PATCH 10/20] maybe_future needs a future! --- jupyterhub/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index b0cc694a..93f56de7 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1675,7 +1675,7 @@ class JupyterHub(Application): try: f = self.authenticator.add_user(user) if f: - await maybe_future() + await maybe_future(f) except Exception: self.log.exception("Error adding user %s already in db", user.name) if self.authenticator.delete_invalid_users: From d2a1b8e349786de66dcf26011f4cfa138113d07b Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 15:31:38 +0100 Subject: [PATCH 11/20] update docs environments - python 3.7 - node 12 - sync recommonmark 0.6 --- docs/environment.yml | 6 +++--- docs/requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index 35b5c405..4ba57441 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -5,12 +5,12 @@ channels: - conda-forge dependencies: - pip -- nodejs -- python=3.6 +- nodejs=12 +- python=3.7 - alembic - jinja2 - pamela -- recommonmark==0.6.0 +- recommonmark>=0.6 - requests - sqlalchemy>=1 - tornado>=5.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index 01d85db1..4934ec4d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,7 +4,7 @@ alabaster_jupyterhub autodoc-traits git+https://github.com/pandas-dev/pandas-sphinx-theme.git@master -recommonmark==0.5.0 +recommonmark>=0.6 sphinx-copybutton sphinx-jsonschema sphinx>=1.7 From 0c4db2d99f5a2ba513cbc6c9efc175d5420272b5 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 15:54:43 +0100 Subject: [PATCH 12/20] docs: use metachannel for faster environment solve rtd is having memory issues with conda-forge, which should hopefully be fixed by metachannel this should also make things quicker for anyone --- docs/environment.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index 4ba57441..0b613bad 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -2,7 +2,10 @@ # if you change the dependencies of JupyterHub in the various `requirements.txt` name: jhub_docs channels: - - conda-forge + # use metachannel for faster solves / less memory on RTD + # which has memory issues with full conda-forge + # only need to list 'leaf' dependencies here + - https://metachannel.conda-forge.org/conda-forge/jupyterhub,sphinx,recommonmark dependencies: - pip - nodejs=12 From 606775f72da2ce648a5a5fbe8e452c72f082e263 Mon Sep 17 00:00:00 2001 From: Tim Head Date: Thu, 20 Feb 2020 16:56:03 +0100 Subject: [PATCH 13/20] Remove unused variable --- jupyterhub/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 93f56de7..41abcf4c 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2019,7 +2019,6 @@ class JupyterHub(Application): if not orm_spawners: continue orm_spawner = orm_spawners[0] - orm_user = orm_spawner.user # instantiate Spawner wrapper and check if it's still alive # spawner should be running user = self.users[orm_spawner.user] From 287b0302d9e6677a826727c973f39e66ea5bc6d5 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 19 Feb 2020 13:46:45 +1300 Subject: [PATCH 14/20] Add more docs about authentication and cookies, using text posted by MinRK on Discourse --- .../getting-started/security-basics.rst | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 7661cfd1..6704e7ce 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -86,7 +86,7 @@ Proxy authentication token -------------------------- The Hub authenticates its requests to the Proxy using a secret token that -the Hub and Proxy agree upon. The value of this string should be a random +the Hub and Proxy agree upon. The value of this token should be a random string (for example, generated by ``openssl rand -hex 32``). Generating and storing token in the configuration file @@ -96,7 +96,7 @@ Or you can set the value in the configuration file, ``jupyterhub_config.py``: .. code-block:: python - c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5' + c.ConfigurableHTTPProxy.api_token = 'abc123...' # any random string Generating and storing as an environment variable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -183,3 +183,70 @@ itself, ``jupyterhub_config.py``, as a binary string: If the cookie secret value changes for the Hub, all single-user notebook servers must also be restarted. + +.. _cookies: + +Cookies used by JupyterHub authentication +----------------------------------------- + +The following cookies are used by the Hub for handling user authentication. + +jupyterhub-hub-login +~~~~~~~~~~~~~~~~~~~~ + +This is the login token used when visiting Hub-served pages encrypted such +as the main home, the spawn form, etc. If this cookie is set, then the +user is logged in. + +Resetting the Hub cookie secret effectively revokes this cookie. + +This cookie is restricted to the path ``/hub/``. + +jupyterhub-user- +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the cookie used for authenticating with a single-user server +encrypted. It is set by the single-user server after OAuth with the Hub. + +Effectively the same as ``jupyterhub-hub-login``, but for the +single-user server instead of the Hub. It contains an OAuth access token, +which is checked with the Hub to authenticate the browser. + +Each OAuth access token is associated with a session id (see ``jupyterhub-session-id`` section +below). + +To avoid hitting the Hub on every request, the authentication response +is cached. And to avoid a stale cache the cache key is comprised of both +the token and session id. + +Resetting the Hub cookie secret effectively revokes this cookie. + +This cookie is restricted to the path ``/user/``, so that +only the user’s server receives it. + +jupyterhub-session-id +~~~~~~~~~~~~~~~~~~~~~ + +This is a random string, meaningless in itself, and the only cookie +shared by the Hub and single-user servers. + +Its sole purpose is to to coordinate logout of the multiple OAuth cookies. + +This cookie is set to ``/`` so all endpoints can receive it, or clear it, etc. + +jupyterhub-user--oauth-state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A short-lived cookie, used solely to store and validate OAuth state. +It is only set while OAuth between the single-user server and the Hub +is processing. + +If you use your browser development tools, you should see this cookie +for a very brief moment before your are logged in, +with an expiration date shorter than ``jupyterhub-hub-login`` or +``jupyterhub-user-``. + +This cookie should not exist after you have successfully logged in. + +This cookie is restricted to the path ``/user/``, so that only +the user’s server receives it. From f5bd5b7751f87dd0ac229be8db909f199cd39a09 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 21 Feb 2020 10:32:11 +1300 Subject: [PATCH 15/20] Incorporate review feedback --- .../getting-started/security-basics.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 6704e7ce..1a79f2e6 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -87,12 +87,13 @@ Proxy authentication token The Hub authenticates its requests to the Proxy using a secret token that the Hub and Proxy agree upon. The value of this token should be a random -string (for example, generated by ``openssl rand -hex 32``). +string (for example, generated by ``openssl rand -hex 32``). You can store +it in the configuration file or an environment variable Generating and storing token in the configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Or you can set the value in the configuration file, ``jupyterhub_config.py``: +You can set the value in the configuration file, ``jupyterhub_config.py``: .. code-block:: python @@ -191,12 +192,16 @@ Cookies used by JupyterHub authentication The following cookies are used by the Hub for handling user authentication. +This section was created based on this post_ from Discourse. + +.. _post: https://discourse.jupyter.org/t/how-to-force-re-login-for-users/1998/6 + jupyterhub-hub-login ~~~~~~~~~~~~~~~~~~~~ -This is the login token used when visiting Hub-served pages encrypted such -as the main home, the spawn form, etc. If this cookie is set, then the -user is logged in. +This is the login token used when visiting Hub-served pages that are +protected by authentication such as the main home, the spawn form, etc. +If this cookie is set, then the user is logged in. Resetting the Hub cookie secret effectively revokes this cookie. @@ -205,8 +210,8 @@ This cookie is restricted to the path ``/hub/``. jupyterhub-user- ~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is the cookie used for authenticating with a single-user server -encrypted. It is set by the single-user server after OAuth with the Hub. +This is the cookie used for authenticating with a single-user server. +It is set by the single-user server after OAuth with the Hub. Effectively the same as ``jupyterhub-hub-login``, but for the single-user server instead of the Hub. It contains an OAuth access token, From 239a4c63a2e644f44297c0f330f8985cd25716b0 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 21 Feb 2020 10:35:30 +1300 Subject: [PATCH 16/20] Add note that not all proxy implementations use an auth token --- docs/source/getting-started/security-basics.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 1a79f2e6..5da0a668 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -86,9 +86,13 @@ Proxy authentication token -------------------------- The Hub authenticates its requests to the Proxy using a secret token that -the Hub and Proxy agree upon. The value of this token should be a random -string (for example, generated by ``openssl rand -hex 32``). You can store -it in the configuration file or an environment variable +the Hub and Proxy agree upon. Note that this applies to the default +``ConfigurableHTTPProxy`` implementation. Not all proxy implementations +use an auth token. + +The value of this token should be a random string (for example, generated by +``openssl rand -hex 32``). You can store it in the configuration file or an +environment variable Generating and storing token in the configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 3e6abb7a5e399b1aebfd85ce388c1d9d1962bfba Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 21 Feb 2020 13:52:03 +0100 Subject: [PATCH 17/20] add general faq and put a first q about user-redirect --- docs/source/getting-started/faq.md | 36 +++++++++++++++++++++++++++ docs/source/getting-started/index.rst | 1 + 2 files changed, 37 insertions(+) create mode 100644 docs/source/getting-started/faq.md diff --git a/docs/source/getting-started/faq.md b/docs/source/getting-started/faq.md new file mode 100644 index 00000000..ae912847 --- /dev/null +++ b/docs/source/getting-started/faq.md @@ -0,0 +1,36 @@ +# Frequently asked questions + + +### How do I share links to notebooks? + +In short, where you see `/user/name/notebooks/foo.ipynb` use `/hub/user-redirect/notebooks/foo.ipynb` (replace `/user/name` with `/hub/user-redirect`). + +Sharing links to notebooks is a common activity, +and can look different based on what you mean. +Your first instinct might be to copy the URL you see in the browser, +e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`. +However, let's break down what this URL means: + +`hub.jupyter.org/user/yourname/` is the URL prefix handled by *your server*, +which means that sharing this URL is asking the person you share the link with +to come to *your server* and look at the exact same file. +In most circumstances, this is forbidden by permissions because the person you share with does not have access to your server. +What actually happens when someone visits this URL will depend on whether your server is running and other factors. + +But what is our actual goal? +A typical situation is that you have some shared or common filesystem, +such that the same path corresponds to the same document +(either the exact same document or another copy of it). +Typically, what folks want when they do sharing like this +is for each visitor to open the same file *on their own server*, +so Breq would open `/user/breq/notebooks/foo.ipynb` and +Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc. + +JupyterHub has a special URL that does exactly this! +It's called `/hub/user-redirect/...` and after the visitor logs in, +So if you replace `/user/yourname` in your URL bar +with `/hub/user-redirect` any visitor should get the same +URL on their own server, rather than visiting yours. + +In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link" +action in the file browser. diff --git a/docs/source/getting-started/index.rst b/docs/source/getting-started/index.rst index 00bcdfbc..bae95f8f 100644 --- a/docs/source/getting-started/index.rst +++ b/docs/source/getting-started/index.rst @@ -15,4 +15,5 @@ own JupyterHub. authenticators-users-basics spawners-basics services-basics + faq institutional-faq From 7e3caf7f4870567775b1b34f95afc5112ecbc541 Mon Sep 17 00:00:00 2001 From: Alex Driedger Date: Sat, 22 Feb 2020 16:37:34 -0800 Subject: [PATCH 18/20] Fixed grammar on landing page --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 1f819278..c9722089 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,7 +3,7 @@ JupyterHub ========== `JupyterHub`_ is the best way to serve `Jupyter notebook`_ for multiple users. -It can be used in a classes of students, a corporate data science group or scientific +It can be used in a class of students, a corporate data science group or scientific research group. It is a multi-user **Hub** that spawns, manages, and proxies multiple instances of the single-user `Jupyter notebook`_ server. From 76afec8adb8e30b6102854ebf79bbe1c0726c27c Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Sat, 12 Oct 2019 12:44:02 +1300 Subject: [PATCH 19/20] Update app.bind_url and proxy.public_url when (external) SSL is enabled --- jupyterhub/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 0cf649bf..dda4c127 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -576,6 +576,22 @@ class JupyterHub(Application): """, ).tag(config=True) + @validate('bind_url') + def _validate_bind_url(self, proposal): + """ensure protocol field of bind_url matches ssl""" + v = proposal['value'] + proto, sep, rest = v.partition('://') + if self.ssl_cert and proto != 'https': + return 'https' + sep + rest + elif proto != 'http' and not self.ssl_cert: + return 'http' + sep + rest + return v + + @default('bind_url') + def _bind_url_default(self): + proto = 'https' if self.ssl_cert else 'http' + return proto + '://:8000' + subdomain_host = Unicode( '', help="""Run single-user servers on subdomains of this host. From 3b05135f11dc3926ff30adf78eae21e767b7a4f3 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 24 Feb 2020 20:48:42 +1300 Subject: [PATCH 20/20] Fix couple typos --- docs/source/contributing/tests.rst | 2 +- docs/source/getting-started/security-basics.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/contributing/tests.rst b/docs/source/contributing/tests.rst index 46360eb7..c607df92 100644 --- a/docs/source/contributing/tests.rst +++ b/docs/source/contributing/tests.rst @@ -64,5 +64,5 @@ Troubleshooting Test Failures All the tests are failing ------------------------- -Make sure you have completed all the steps in :ref:`contributing/setup` sucessfully, and +Make sure you have completed all the steps in :ref:`contributing/setup` successfully, and can launch ``jupyterhub`` from the terminal. diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 5da0a668..87007311 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -239,7 +239,7 @@ jupyterhub-session-id This is a random string, meaningless in itself, and the only cookie shared by the Hub and single-user servers. -Its sole purpose is to to coordinate logout of the multiple OAuth cookies. +Its sole purpose is to coordinate logout of the multiple OAuth cookies. This cookie is set to ``/`` so all endpoints can receive it, or clear it, etc.