Skip to content

Commit cd1d3a9

Browse files
committed
wip
1 parent 1ba842f commit cd1d3a9

File tree

3 files changed

+147
-30
lines changed

3 files changed

+147
-30
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ classifiers = [
3636
dependencies = [
3737
"jupyter_client>=6.1.3,<=7.4.1",
3838
"jupyter_core>=4.11.0",
39-
"jupyter_server>=1.18,<2.0.0",
39+
"jupyter_server>=2.0.0,<3",
4040
"jupyterlab_server>=2.3.0,<3",
4141
"nbclient>=0.4.0,<0.8",
4242
"nbconvert>=6.4.5,<8",

voila/app.py

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import threading
2121
import webbrowser
2222

23+
from .voila_identity_provider import VoilaLoginHandler
24+
2325
try:
2426
from urllib.parse import urljoin
2527
from urllib.request import pathname2url
@@ -35,12 +37,29 @@
3537
from jupyter_core.paths import jupyter_config_path, jupyter_path
3638
from jupyter_server.base.handlers import FileFindHandler, path_regex
3739
from jupyter_server.config_manager import recursive_update
38-
from jupyter_server.services.config import ConfigManager
40+
from jupyter_server.services.config.manager import ConfigManager
3941
from jupyter_server.services.contents.largefilemanager import LargeFileManager
40-
from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
41-
from jupyter_server.utils import run_sync, url_path_join
42+
from jupyter_server.services.kernels.handlers import KernelHandler
43+
from jupyter_server.services.kernels.websocket import KernelWebsocketHandler
44+
from jupyter_server.auth.authorizer import AllowAllAuthorizer, Authorizer
45+
from jupyter_server.auth.identity import PasswordIdentityProvider
46+
from jupyter_server import DEFAULT_TEMPLATE_PATH_LIST, DEFAULT_STATIC_FILES_PATH
47+
from jupyter_server.services.kernels.connection.base import (
48+
BaseKernelWebsocketConnection,
49+
)
50+
from jupyter_server.services.kernels.connection.channels import (
51+
ZMQChannelsWebsocketConnection,
52+
)
53+
from jupyter_server.auth.identity import (
54+
IdentityProvider,
55+
)
56+
from jupyter_server.utils import url_path_join
57+
from jupyter_core.utils import run_sync
58+
4259
from jupyterlab_server.themes_handler import ThemesHandler
43-
from traitlets import Bool, Callable, Dict, Integer, List, Unicode, default
60+
61+
62+
from traitlets import Bool, Callable, Dict, Integer, List, Unicode, default, Type, Bytes
4463
from traitlets.config.application import Application
4564
from traitlets.config.loader import Config
4665

@@ -272,6 +291,43 @@ def hook(req: tornado.web.RequestHandler,
272291
),
273292
)
274293

294+
cookie_secret = Bytes(
295+
b"",
296+
config=True,
297+
help="""The random bytes used to secure cookies.
298+
By default this is a new random number every time you start the server.
299+
Set it to a value in a config file to enable logins to persist across server sessions.
300+
301+
Note: Cookie secrets should be kept private, do not share config files with
302+
cookie_secret stored in plaintext (you can read the value from a file).
303+
""",
304+
)
305+
306+
@default("cookie_secret")
307+
def _default_cookie_secret(self):
308+
return os.urandom(32)
309+
310+
authorizer_class = Type(
311+
default_value=AllowAllAuthorizer,
312+
klass=Authorizer,
313+
config=True,
314+
help=_("The authorizer class to use."),
315+
)
316+
317+
identity_provider_class = Type(
318+
default_value=PasswordIdentityProvider,
319+
klass=IdentityProvider,
320+
config=True,
321+
help=_("The identity provider class to use."),
322+
)
323+
324+
kernel_websocket_connection_class = Type(
325+
default_value=ZMQChannelsWebsocketConnection,
326+
klass=BaseKernelWebsocketConnection,
327+
config=True,
328+
help=_("The kernel websocket connection class to use."),
329+
)
330+
275331
@property
276332
def display_url(self):
277333
if self.custom_display_url:
@@ -282,13 +338,17 @@ def display_url(self):
282338
ip = "%s" % socket.gethostname() if self.ip in ("", "0.0.0.0") else self.ip
283339
url = self._url(ip)
284340
# TODO: do we want to have the token?
285-
# if self.token:
286-
# # Don't log full token if it came from config
287-
# token = self.token if self._token_generated else '...'
288-
# url = (url_concat(url, {'token': token})
289-
# + '\n or '
290-
# + url_concat(self._url('127.0.0.1'), {'token': token}))
291-
return url
341+
if self.identity_provider.token:
342+
# Don't log full token if it came from config
343+
token = (
344+
self.identity_provider.token
345+
if self.identity_provider.token_generated
346+
else "..."
347+
)
348+
query = f"?token={token}"
349+
else:
350+
query = ""
351+
return f"{url}{query}"
292352

293353
@property
294354
def connection_url(self):
@@ -405,6 +465,7 @@ def setup_template_dirs(self):
405465
self.static_paths = collect_static_paths(
406466
["voila", "nbconvert"], template_name
407467
)
468+
self.static_paths.append(DEFAULT_STATIC_FILES_PATH)
408469
conf_paths = [os.path.join(d, "conf.json") for d in self.template_paths]
409470
for p in conf_paths:
410471
# see if config file exists
@@ -428,17 +489,8 @@ def setup_template_dirs(self):
428489
if self.notebook_path and not os.path.exists(self.notebook_path):
429490
raise ValueError("Notebook not found: %s" % self.notebook_path)
430491

431-
def _handle_signal_stop(self, sig, frame):
432-
self.log.info("Handle signal %s." % sig)
433-
self.ioloop.add_callback_from_signal(self.ioloop.stop)
434-
435-
def start(self):
436-
self.connection_dir = tempfile.mkdtemp(
437-
prefix="voila_", dir=self.connection_dir_root
438-
)
439-
self.log.info("Storing connection files in %s." % self.connection_dir)
440-
self.log.info("Serving static files from %s." % self.static_root)
441-
492+
def init_settings(self) -> Dict:
493+
"""Initialize settings for Voila application."""
442494
# default server_url to base_url
443495
self.server_url = self.server_url or self.base_url
444496

@@ -486,28 +538,55 @@ def start(self):
486538
extensions=["jinja2.ext.i18n"],
487539
**jenv_opt,
488540
)
541+
server_env = jinja2.Environment(
542+
loader=jinja2.FileSystemLoader(DEFAULT_TEMPLATE_PATH_LIST),
543+
extensions=["jinja2.ext.i18n"],
544+
**jenv_opt,
545+
)
546+
489547
nbui = gettext.translation(
490548
"nbui", localedir=os.path.join(ROOT, "i18n"), fallback=True
491549
)
492550
env.install_gettext_translations(nbui, newstyle=False)
551+
server_env.install_gettext_translations(nbui, newstyle=False)
552+
553+
identity_provider_kwargs = {
554+
"parent": self,
555+
"log": self.log,
556+
"login_handler_class": VoilaLoginHandler,
557+
}
558+
self.identity_provider = self.identity_provider_class(
559+
**identity_provider_kwargs
560+
)
493561

494-
self.app = tornado.web.Application(
562+
self.authorizer = self.authorizer_class(
563+
parent=self, log=self.log, identity_provider=self.identity_provider
564+
)
565+
566+
settings = dict(
495567
base_url=self.base_url,
496568
server_url=self.server_url or self.base_url,
497569
kernel_manager=self.kernel_manager,
498570
kernel_spec_manager=self.kernel_spec_manager,
499571
allow_remote_access=True,
500572
autoreload=self.autoreload,
501573
voila_jinja2_env=env,
502-
jinja2_env=env,
574+
jinja2_env=server_env,
503575
static_path="/",
504576
server_root_dir="/",
505577
contents_manager=self.contents_manager,
506578
config_manager=self.config_manager,
579+
cookie_secret=self.cookie_secret,
580+
authorizer=self.authorizer,
581+
identity_provider=self.identity_provider,
582+
kernel_websocket_connection_class=self.kernel_websocket_connection_class,
583+
login_url=url_path_join(self.base_url, "/login"),
507584
)
508585

509-
self.app.settings.update(self.tornado_settings)
586+
return settings
510587

588+
def init_handlers(self) -> List:
589+
"""Initialize handlers for Voila application."""
511590
handlers = []
512591

513592
handlers.extend(
@@ -522,7 +601,7 @@ def start(self):
522601
url_path_join(
523602
self.server_url, r"/api/kernels/%s/channels" % _kernel_id_regex
524603
),
525-
ZMQChannelsHandler,
604+
KernelWebsocketHandler,
526605
),
527606
(
528607
url_path_join(self.server_url, r"/voila/templates/(.*)"),
@@ -549,8 +628,8 @@ def start(self):
549628
),
550629
]
551630
)
552-
553-
if preheat_kernel:
631+
handlers.extend(self.identity_provider.get_handlers())
632+
if self.voila_configuration.preheat_kernel:
554633
handlers.append(
555634
(
556635
url_path_join(
@@ -621,13 +700,30 @@ def start(self):
621700
),
622701
]
623702
)
703+
return handlers
704+
705+
def start(self):
706+
self.connection_dir = tempfile.mkdtemp(
707+
prefix="voila_", dir=self.connection_dir_root
708+
)
709+
self.log.info("Storing connection files in %s." % self.connection_dir)
710+
self.log.info("Serving static files from %s." % self.static_root)
711+
712+
settings = self.init_settings()
624713

714+
self.app = tornado.web.Application(**settings)
715+
self.app.settings.update(self.tornado_settings)
716+
handlers = self.init_handlers()
625717
self.app.add_handlers(".*$", handlers)
626718
self.listen()
627719

720+
def _handle_signal_stop(self, sig, frame):
721+
self.log.info("Handle signal %s." % sig)
722+
self.ioloop.add_callback_from_signal(self.ioloop.stop)
723+
628724
def stop(self):
629725
shutil.rmtree(self.connection_dir)
630-
run_sync(self.kernel_manager.shutdown_all())
726+
run_sync(self.kernel_manager.shutdown_all)()
631727

632728
def random_ports(self, port, n):
633729
"""Generate a list of n random ports near the given port.

voila/voila_identity_provider.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Any, Optional
2+
from jupyter_server.auth.identity import IdentityProvider
3+
from jupyter_server.auth.login import LoginFormHandler
4+
5+
6+
class VoilaLoginHandler(LoginFormHandler):
7+
def static_url(
8+
self, path: str, include_host: Optional[bool] = None, **kwargs: Any
9+
) -> str:
10+
settings = {
11+
"static_url_prefix": "voila/static/",
12+
"static_path": None,
13+
}
14+
return settings.get("static_url_prefix", "/static/") + path
15+
16+
17+
class VoilaIdentityProvider(IdentityProvider):
18+
@property
19+
def auth_enabled(self) -> bool:
20+
"""Return whether any auth is enabled"""
21+
return bool(self.token)

0 commit comments

Comments
 (0)