mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 03:52:59 +00:00
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:
2
jupyterhub/singleuser/__init__.py
Normal file
2
jupyterhub/singleuser/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .notebookapp import main
|
||||||
|
from .notebookapp import SingleUserNotebookApp
|
4
jupyterhub/singleuser/__main__.py
Normal file
4
jupyterhub/singleuser/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .notebookapp import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@@ -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,27 +128,22 @@ 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',
|
||||||
{
|
'group': 'SingleUserNotebookApp.group',
|
||||||
'user': 'SingleUserNotebookApp.user',
|
'cookie-name': 'HubAuth.cookie_name',
|
||||||
'group': 'SingleUserNotebookApp.group',
|
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
||||||
'cookie-name': 'HubAuth.cookie_name',
|
'hub-host': 'SingleUserNotebookApp.hub_host',
|
||||||
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
'hub-api-url': 'SingleUserNotebookApp.hub_api_url',
|
||||||
'hub-host': 'SingleUserNotebookApp.hub_host',
|
'base-url': 'SingleUserNotebookApp.base_url',
|
||||||
'hub-api-url': 'SingleUserNotebookApp.hub_api_url',
|
}
|
||||||
'base-url': 'SingleUserNotebookApp.base_url',
|
flags = {
|
||||||
}
|
'disable-user-config': (
|
||||||
)
|
{'SingleUserNotebookApp': {'disable_user_config': True}},
|
||||||
flags = dict(notebook_flags)
|
"Disable user-controlled configuration of the notebook server.",
|
||||||
flags.update(
|
)
|
||||||
{
|
}
|
||||||
'disable-user-config': (
|
|
||||||
{'SingleUserNotebookApp': {'disable_user_config': True}},
|
|
||||||
"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
|
22
jupyterhub/singleuser/notebookapp.py
Executable file
22
jupyterhub/singleuser/notebookapp.py
Executable 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()
|
Reference in New Issue
Block a user