create singleuser app with mixins

for easier reuse with jupyter_server

mixins have a lot of assumptions about the NotebookApp structure.
Need to make sure these are met by jupyter_server (that's what tests are for!)
This commit is contained in:
Min RK
2020-07-24 11:27:19 +02:00
parent 2372842b8a
commit a0a02688c5
4 changed files with 146 additions and 95 deletions

View File

@@ -0,0 +1,2 @@
from .notebookapp import main
from .notebookapp import SingleUserNotebookApp

View File

@@ -0,0 +1,4 @@
from .notebookapp import main
if __name__ == '__main__':
main()

View File

@@ -1,5 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Extend regular notebook server to be aware of multiuser things.""" """Mixins to regular notebook server to add JupyterHub auth.
Meant to be compatible with jupyter_server and classic notebook
Use make_singleuser_app to create a compatible Application class
with JupyterHub authentication mixins enabled.
"""
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import asyncio import asyncio
@@ -20,58 +26,28 @@ from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPRequest from tornado.httpclient import HTTPRequest
from tornado.web import HTTPError from tornado.web import HTTPError
from tornado.web import RequestHandler from tornado.web import RequestHandler
from traitlets import Any
from traitlets import Bool
from traitlets import Bytes
from traitlets import CUnicode
from traitlets import default
from traitlets import Integer
from traitlets import observe
from traitlets import TraitError
from traitlets import Unicode
from traitlets import validate
from traitlets.config import Configurable
try: from .._version import __version__
import notebook from .._version import _check_version
from ..log import log_request
use_serverapp = False from ..services.auth import HubOAuth
server_package = 'notebook' from ..services.auth import HubOAuthCallbackHandler
app_name = 'notebook.notebookapp' from ..services.auth import HubOAuthenticated
except ImportError: from ..utils import exponential_backoff
try: from ..utils import isoformat
import jupyter_server from ..utils import make_ssl_context
from ..utils import url_path_join
use_serverapp = True
server_package = 'jupyter_server'
app_name = 'jupyter_server.serverapp'
except ImportError:
raise ImportError(
"JupyterHub single-user server requires notebook or jupyter_server packages"
)
from traitlets import (
Any,
Bool,
Bytes,
Integer,
Unicode,
CUnicode,
default,
observe,
validate,
TraitError,
)
app_module = importlib.import_module(app_name)
NotebookApp = getattr(app_module, 'ServerApp' if use_serverapp else 'NotebookApp')
notebook_aliases = app_module.aliases
notebook_flags = app_module.flags
LoginHandler = getattr(
importlib.import_module(server_package + '.auth.login'), 'LoginHandler'
)
LogoutHandler = getattr(
importlib.import_module(server_package + '.auth.logout'), 'LogoutHandler'
)
IPythonHandler = getattr(
importlib.import_module(server_package + '.base.handlers'),
'JupyterHandler' if use_serverapp else 'IPythonHandler',
)
from ._version import __version__, _check_version
from .log import log_request
from .services.auth import HubOAuth, HubOAuthenticated, HubOAuthCallbackHandler
from .utils import isoformat, url_path_join, make_ssl_context, exponential_backoff
# Authenticate requests with the Hub # Authenticate requests with the Hub
@@ -101,7 +77,7 @@ class HubAuthenticatedHandler(HubOAuthenticated):
return set() return set()
class JupyterHubLoginHandler(LoginHandler): class JupyterHubLoginHandlerMixin:
"""LoginHandler that hooks up Hub authentication""" """LoginHandler that hooks up Hub authentication"""
@staticmethod @staticmethod
@@ -134,7 +110,7 @@ class JupyterHubLoginHandler(LoginHandler):
return return
class JupyterHubLogoutHandler(LogoutHandler): class JupyterHubLogoutHandlerMixin:
def get(self): def get(self):
self.settings['hub_auth'].clear_cookie(self) self.settings['hub_auth'].clear_cookie(self)
self.redirect( self.redirect(
@@ -143,7 +119,7 @@ class JupyterHubLogoutHandler(LogoutHandler):
) )
class OAuthCallbackHandler(HubOAuthCallbackHandler, IPythonHandler): class OAuthCallbackHandlerMixin(HubOAuthCallbackHandler):
"""Mixin IPythonHandler to get the right error pages, etc.""" """Mixin IPythonHandler to get the right error pages, etc."""
@property @property
@@ -152,9 +128,7 @@ class OAuthCallbackHandler(HubOAuthCallbackHandler, IPythonHandler):
# register new hub related command-line aliases # register new hub related command-line aliases
aliases = dict(notebook_aliases) aliases = {
aliases.update(
{
'user': 'SingleUserNotebookApp.user', 'user': 'SingleUserNotebookApp.user',
'group': 'SingleUserNotebookApp.group', 'group': 'SingleUserNotebookApp.group',
'cookie-name': 'HubAuth.cookie_name', 'cookie-name': 'HubAuth.cookie_name',
@@ -162,17 +136,14 @@ aliases.update(
'hub-host': 'SingleUserNotebookApp.hub_host', 'hub-host': 'SingleUserNotebookApp.hub_host',
'hub-api-url': 'SingleUserNotebookApp.hub_api_url', 'hub-api-url': 'SingleUserNotebookApp.hub_api_url',
'base-url': 'SingleUserNotebookApp.base_url', 'base-url': 'SingleUserNotebookApp.base_url',
} }
) flags = {
flags = dict(notebook_flags)
flags.update(
{
'disable-user-config': ( 'disable-user-config': (
{'SingleUserNotebookApp': {'disable_user_config': True}}, {'SingleUserNotebookApp': {'disable_user_config': True}},
"Disable user-controlled configuration of the notebook server.", "Disable user-controlled configuration of the notebook server.",
) )
} }
)
page_template = """ page_template = """
{% extends "templates/page.html" %} {% extends "templates/page.html" %}
@@ -237,21 +208,32 @@ def _exclude_home(path_list):
yield p yield p
class SingleUserNotebookApp(NotebookApp): from traitlets import HasTraits
class SingleUserNotebookAppMixin(Configurable):
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context.""" """A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
description = dedent( description = dedent(
""" """
Single-user server for JupyterHub. Extends the Jupyter Notebook server. Single-user server for JupyterHub. Extends the Jupyter Notebook server.
Meant to be invoked by JupyterHub Spawners, and not directly. Meant to be invoked by JupyterHub Spawners, not directly.
""" """
) )
examples = "" examples = ""
subcommands = {} subcommands = {}
version = __version__ version = __version__
classes = NotebookApp.classes + [HubOAuth]
# must be set in mixin subclass
# make_singleuser_app sets these
# aliases = aliases
# flags = flags
# login_handler_class = JupyterHubLoginHandler
# logout_handler_class = JupyterHubLogoutHandler
# oauth_callback_handler_class = OAuthCallbackHandler
# classes = NotebookApp.classes + [HubOAuth]
# disable single-user app's localhost checking # disable single-user app's localhost checking
allow_remote_access = True allow_remote_access = True
@@ -344,16 +326,12 @@ class SingleUserNotebookApp(NotebookApp):
return url.hostname return url.hostname
return '127.0.0.1' return '127.0.0.1'
aliases = aliases # disable some single-user configurables
flags = flags
# disble some single-user configurables
token = '' token = ''
open_browser = False open_browser = False
quit_button = False quit_button = False
trust_xheaders = True trust_xheaders = True
login_handler_class = JupyterHubLoginHandler
logout_handler_class = JupyterHubLogoutHandler
port_retries = ( port_retries = (
0 # disable port-retries, since the Spawner will tell us what port to use 0 # disable port-retries, since the Spawner will tell us what port to use
) )
@@ -402,11 +380,11 @@ class SingleUserNotebookApp(NotebookApp):
# disable config-migration when user config is disabled # disable config-migration when user config is disabled
return return
else: else:
super(SingleUserNotebookApp, self).migrate_config() super().migrate_config()
@property @property
def config_file_paths(self): def config_file_paths(self):
path = super(SingleUserNotebookApp, self).config_file_paths path = super().config_file_paths
if self.disable_user_config: if self.disable_user_config:
# filter out user-writable config dirs if user config is disabled # filter out user-writable config dirs if user config is disabled
@@ -415,7 +393,7 @@ class SingleUserNotebookApp(NotebookApp):
@property @property
def nbextensions_path(self): def nbextensions_path(self):
path = super(SingleUserNotebookApp, self).nbextensions_path path = super().nbextensions_path
if self.disable_user_config: if self.disable_user_config:
path = list(_exclude_home(path)) path = list(_exclude_home(path))
@@ -583,7 +561,7 @@ class SingleUserNotebookApp(NotebookApp):
# start by hitting Hub to check version # start by hitting Hub to check version
ioloop.IOLoop.current().run_sync(self.check_hub_version) ioloop.IOLoop.current().run_sync(self.check_hub_version)
ioloop.IOLoop.current().add_callback(self.keep_activity_updated) ioloop.IOLoop.current().add_callback(self.keep_activity_updated)
super(SingleUserNotebookApp, self).start() super().start()
def init_hub_auth(self): def init_hub_auth(self):
api_token = None api_token = None
@@ -631,12 +609,17 @@ class SingleUserNotebookApp(NotebookApp):
'Content-Security-Policy', 'Content-Security-Policy',
';'.join(["frame-ancestors 'self'", "report-uri " + csp_report_uri]), ';'.join(["frame-ancestors 'self'", "report-uri " + csp_report_uri]),
) )
super(SingleUserNotebookApp, self).init_webapp() super().init_webapp()
# add OAuth callback # add OAuth callback
self.web_app.add_handlers( self.web_app.add_handlers(
r".*$", r".*$",
[(urlparse(self.hub_auth.oauth_redirect_uri).path, OAuthCallbackHandler)], [
(
urlparse(self.hub_auth.oauth_redirect_uri).path,
self.oauth_callback_handler_class,
)
],
) )
# apply X-JupyterHub-Version to *all* request handlers (even redirects) # apply X-JupyterHub-Version to *all* request handlers (even redirects)
@@ -677,9 +660,49 @@ class SingleUserNotebookApp(NotebookApp):
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader]) env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
def main(argv=None): def make_singleuser_app(App, LoginHandler, LogoutHandler, BaseHandler):
return SingleUserNotebookApp.launch_instance(argv) """Make and return a singleuser notebook app
given existing notebook or jupyter_server classes,
mix-in jupyterhub auth.
if __name__ == "__main__": App should be a subclass of `notebook.notebookapp.NotebookApp`
main() or `jupyter_server.serverapp.ServerApp`
Must be passed base classes for:
- App
- LoginHandler
- LogoutHandler
- BaseHandler
"""
# create handler classes from mixins + bases
class JupyterHubLoginHandler(JupyterHubLoginHandlerMixin, LoginHandler):
pass
class JupyterHubLogoutHandler(JupyterHubLogoutHandlerMixin, LogoutHandler):
pass
class OAuthCallbackHandler(OAuthCallbackHandlerMixin, BaseHandler):
pass
# create merged aliases & flags
empty_parent_app = App()
merged_aliases = {}
merged_aliases.update(empty_parent_app.aliases)
merged_aliases.update(aliases)
merged_flags = {}
merged_flags.update(empty_parent_app.flags)
merged_flags.update(flags)
# create mixed-in App class, bringing it all together
class SingleUserNotebookApp(SingleUserNotebookAppMixin, App):
aliases = merged_aliases
flags = merged_flags
classes = empty_parent_app.classes + [HubOAuth]
login_handler_class = JupyterHubLoginHandler
logout_handler_class = JupyterHubLogoutHandler
oauth_callback_handler_class = OAuthCallbackHandler
return SingleUserNotebookApp

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Extend regular notebook server to be aware of multiuser things."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from notebook.auth.login import LoginHandler
from notebook.auth.logout import LogoutHandler
from notebook.base.handlers import IPythonHandler
from notebook.notebookapp import NotebookApp
from .mixins import make_singleuser_app
SingleUserNotebookApp = make_singleuser_app(
NotebookApp=NotebookApp,
LoginHandler=LoginHandler,
LogoutHandler=LogoutHandler,
BaseHandler=IPythonHandler,
)
main = SingleUserNotebookApp.launch_instance
if __name__ == '__main__':
main()