expose cookie options and pass them down to spawners

enables forcing all-session cookies with:

```python
c.JupyterHub.tornado_settings['cookie_options'] = {
    'expires_days': None,
}
```
This commit is contained in:
Min RK
2018-03-23 10:38:50 +01:00
parent 82cab39e1c
commit e374e93cfb
4 changed files with 37 additions and 5 deletions

View File

@@ -30,7 +30,10 @@ from tornado.httputil import url_concat
from tornado.web import HTTPError, RequestHandler from tornado.web import HTTPError, RequestHandler
from traitlets.config import SingletonConfigurable from traitlets.config import SingletonConfigurable
from traitlets import Unicode, Integer, Instance, default, observe, validate from traitlets import (
Unicode, Integer, Instance, Dict,
default, observe, validate,
)
from ..utils import url_path_join from ..utils import url_path_join
@@ -175,7 +178,7 @@ class HubAuth(SingletonConfigurable):
hub_prefix = Unicode('/hub/', hub_prefix = Unicode('/hub/',
help="""The URL prefix for the Hub itself. help="""The URL prefix for the Hub itself.
Typically /hub/ Typically /hub/
""" """
).tag(config=True) ).tag(config=True)
@@ -185,7 +188,7 @@ class HubAuth(SingletonConfigurable):
login_url = Unicode('/hub/login', login_url = Unicode('/hub/login',
help="""The login URL to use help="""The login URL to use
Typically /hub/login Typically /hub/login
""" """
).tag(config=True) ).tag(config=True)
@@ -197,6 +200,24 @@ class HubAuth(SingletonConfigurable):
help="""The name of the cookie I should be looking for""" help="""The name of the cookie I should be looking for"""
).tag(config=True) ).tag(config=True)
cookie_options = Dict(
help="""Additional options to pass when setting cookies.
Can include things like `expires_days=None` for session-expiry
or `secure=True` if served on HTTPS and default HTTPS discovery fails
(e.g. behind some proxies).
"""
).tag(config=True)
@default('cookie_options')
def _default_cookie_options(self):
# load default from env
options_env = os.environ.get('JUPYTERHUB_COOKIE_OPTIONS')
if options_env:
return json.loads(options_env)
else:
return {}
cookie_cache_max_age = Integer(help="DEPRECATED. Use cache_max_age") cookie_cache_max_age = Integer(help="DEPRECATED. Use cache_max_age")
@observe('cookie_cache_max_age') @observe('cookie_cache_max_age')
def _deprecated_cookie_cache(self, change): def _deprecated_cookie_cache(self, change):
@@ -580,6 +601,8 @@ class HubOAuth(HubAuth):
} }
if handler.request.protocol == 'https': if handler.request.protocol == 'https':
kwargs['secure'] = True kwargs['secure'] = True
# load user cookie overrides
kwargs.update(self.cookie_options)
handler.set_secure_cookie( handler.set_secure_cookie(
cookie_name, cookie_name,
b64_state, b64_state,
@@ -627,6 +650,8 @@ class HubOAuth(HubAuth):
} }
if handler.request.protocol == 'https': if handler.request.protocol == 'https':
kwargs['secure'] = True kwargs['secure'] = True
# load user cookie overrides
kwargs.update(self.cookie_options)
app_log.debug("Setting oauth cookie for %s: %s, %s", app_log.debug("Setting oauth cookie for %s: %s, %s",
handler.request.remote_ip, self.cookie_name, kwargs) handler.request.remote_ip, self.cookie_name, kwargs)
handler.set_secure_cookie( handler.set_secure_cookie(

View File

@@ -218,6 +218,7 @@ class Service(LoggingConfigurable):
base_url = Unicode() base_url = Unicode()
db = Any() db = Any()
orm = Any() orm = Any()
cookie_options = Dict()
oauth_provider = Any() oauth_provider = Any()
@@ -299,6 +300,7 @@ class Service(LoggingConfigurable):
environment=env, environment=env,
api_token=self.api_token, api_token=self.api_token,
oauth_client_id=self.oauth_client_id, oauth_client_id=self.oauth_client_id,
cookie_options=self.cookie_options,
cwd=self.cwd, cwd=self.cwd,
hub=self.hub, hub=self.hub,
user=_MockUser( user=_MockUser(

View File

@@ -6,6 +6,7 @@ Contains base Spawner class & default implementation
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import errno import errno
import json
import os import os
import pipes import pipes
import shutil import shutil
@@ -99,11 +100,12 @@ class Spawner(LoggingConfigurable):
""" """
return bool(self.pending or self.ready) return bool(self.pending or self.ready)
# options passed by constructor
authenticator = Any() authenticator = Any()
hub = Any() hub = Any()
orm_spawner = Any() orm_spawner = Any()
db = Any() db = Any()
cookie_options = Dict()
@observe('orm_spawner') @observe('orm_spawner')
def _orm_spawner_changed(self, change): def _orm_spawner_changed(self, change):
@@ -125,7 +127,7 @@ class Spawner(LoggingConfigurable):
if missing: if missing:
raise NotImplementedError("class `{}` needs to redefine the `start`," raise NotImplementedError("class `{}` needs to redefine the `start`,"
"`stop` and `poll` methods. `{}` not redefined.".format(cls.__name__, '`, `'.join(missing))) "`stop` and `poll` methods. `{}` not redefined.".format(cls.__name__, '`, `'.join(missing)))
proxy_spec = Unicode() proxy_spec = Unicode()
@property @property
@@ -587,6 +589,8 @@ class Spawner(LoggingConfigurable):
env['JUPYTERHUB_ADMIN_ACCESS'] = '1' env['JUPYTERHUB_ADMIN_ACCESS'] = '1'
# OAuth settings # OAuth settings
env['JUPYTERHUB_CLIENT_ID'] = self.oauth_client_id env['JUPYTERHUB_CLIENT_ID'] = self.oauth_client_id
if self.cookie_options:
env['JUPYTERHUB_COOKIE_OPTIONS'] = json.dumps(self.cookie_options)
env['JUPYTERHUB_HOST'] = self.hub.public_host env['JUPYTERHUB_HOST'] = self.hub.public_host
env['JUPYTERHUB_OAUTH_CALLBACK_URL'] = \ env['JUPYTERHUB_OAUTH_CALLBACK_URL'] = \
url_path_join(self.user.url, self.name, 'oauth_callback') url_path_join(self.user.url, self.name, 'oauth_callback')

View File

@@ -215,6 +215,7 @@ class User:
proxy_spec=url_path_join(self.proxy_spec, name, '/'), proxy_spec=url_path_join(self.proxy_spec, name, '/'),
db=self.db, db=self.db,
oauth_client_id=client_id, oauth_client_id=client_id,
cookie_options = self.settings.get('cookie_options', {}),
) )
# update with kwargs. Mainly for testing. # update with kwargs. Mainly for testing.
spawn_kwargs.update(kwargs) spawn_kwargs.update(kwargs)