Skip to content

Commit 1286f00

Browse files
trungleducjabbera
andcommitted
Get changes from #1283
Co-authored-by: Mike <[email protected]>
1 parent 1ba842f commit 1286f00

File tree

6 files changed

+174
-257
lines changed

6 files changed

+174
-257
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
os: [ubuntu-latest]
21-
python_version: ['3.7', '3.8', '3.9', '3.10']
21+
python_version: ['3.8', '3.9', '3.10']
2222

2323
steps:
2424
- uses: actions/checkout@v2

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
1010
name = "voila"
1111
description = "Voilà turns Jupyter notebooks into standalone web applications"
1212
readme = "README.md"
13-
requires-python = ">=3.7"
13+
requires-python = ">=3.8"
1414
authors = [
1515
{ name = "Voila Development Team" },
1616
]
@@ -34,9 +34,9 @@ classifiers = [
3434
"Programming Language :: Python :: 3.10",
3535
]
3636
dependencies = [
37-
"jupyter_client>=6.1.3,<=7.4.1",
37+
"jupyter_client>=7.4.4,<9",
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",

tests/app/shutdown_kernel_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import re
2+
import pytest
3+
4+
5+
@pytest.fixture
6+
def voila_config():
7+
def config(voila_app):
8+
voila_app.tornado_settings["disable_check_xsrf"] = True
9+
10+
return config
211

312

413
async def test_shutdown_handler(http_server_client, base_url):

voila/app.py

Lines changed: 133 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

@@ -130,6 +149,7 @@ class Voila(Application):
130149
"port": "Voila.port",
131150
"static": "Voila.static_root",
132151
"server_url": "Voila.server_url",
152+
"token": "Voila.token",
133153
"pool_size": "VoilaConfiguration.default_pool_size",
134154
"show_tracebacks": "VoilaConfiguration.show_tracebacks",
135155
"preheat_kernel": "VoilaConfiguration.preheat_kernel",
@@ -272,6 +292,47 @@ def hook(req: tornado.web.RequestHandler,
272292
),
273293
)
274294

295+
cookie_secret = Bytes(
296+
b"",
297+
config=True,
298+
help="""The random bytes used to secure cookies.
299+
By default this is a new random number every time you start the server.
300+
Set it to a value in a config file to enable logins to persist across server sessions.
301+
302+
Note: Cookie secrets should be kept private, do not share config files with
303+
cookie_secret stored in plaintext (you can read the value from a file).
304+
""",
305+
)
306+
307+
token = Unicode(None, help="""Token for identity provider """, allow_none=True).tag(
308+
config=True
309+
)
310+
311+
@default("cookie_secret")
312+
def _default_cookie_secret(self):
313+
return os.urandom(32)
314+
315+
authorizer_class = Type(
316+
default_value=AllowAllAuthorizer,
317+
klass=Authorizer,
318+
config=True,
319+
help=_("The authorizer class to use."),
320+
)
321+
322+
identity_provider_class = Type(
323+
default_value=PasswordIdentityProvider,
324+
klass=IdentityProvider,
325+
config=True,
326+
help=_("The identity provider class to use."),
327+
)
328+
329+
kernel_websocket_connection_class = Type(
330+
default_value=ZMQChannelsWebsocketConnection,
331+
klass=BaseKernelWebsocketConnection,
332+
config=True,
333+
help=_("The kernel websocket connection class to use."),
334+
)
335+
275336
@property
276337
def display_url(self):
277338
if self.custom_display_url:
@@ -282,13 +343,17 @@ def display_url(self):
282343
ip = "%s" % socket.gethostname() if self.ip in ("", "0.0.0.0") else self.ip
283344
url = self._url(ip)
284345
# 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
346+
if self.identity_provider.token:
347+
# Don't log full token if it came from config
348+
token = (
349+
self.identity_provider.token
350+
if self.identity_provider.token_generated
351+
else "..."
352+
)
353+
query = f"?token={token}"
354+
else:
355+
query = ""
356+
return f"{url}{query}"
292357

293358
@property
294359
def connection_url(self):
@@ -405,6 +470,7 @@ def setup_template_dirs(self):
405470
self.static_paths = collect_static_paths(
406471
["voila", "nbconvert"], template_name
407472
)
473+
self.static_paths.append(DEFAULT_STATIC_FILES_PATH)
408474
conf_paths = [os.path.join(d, "conf.json") for d in self.template_paths]
409475
for p in conf_paths:
410476
# see if config file exists
@@ -428,17 +494,8 @@ def setup_template_dirs(self):
428494
if self.notebook_path and not os.path.exists(self.notebook_path):
429495
raise ValueError("Notebook not found: %s" % self.notebook_path)
430496

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-
497+
def init_settings(self) -> Dict:
498+
"""Initialize settings for Voila application."""
442499
# default server_url to base_url
443500
self.server_url = self.server_url or self.base_url
444501

@@ -486,28 +543,58 @@ def start(self):
486543
extensions=["jinja2.ext.i18n"],
487544
**jenv_opt,
488545
)
546+
server_env = jinja2.Environment(
547+
loader=jinja2.FileSystemLoader(DEFAULT_TEMPLATE_PATH_LIST),
548+
extensions=["jinja2.ext.i18n"],
549+
**jenv_opt,
550+
)
551+
489552
nbui = gettext.translation(
490553
"nbui", localedir=os.path.join(ROOT, "i18n"), fallback=True
491554
)
492555
env.install_gettext_translations(nbui, newstyle=False)
556+
server_env.install_gettext_translations(nbui, newstyle=False)
557+
558+
identity_provider_kwargs = {
559+
"parent": self,
560+
"log": self.log,
561+
"login_handler_class": VoilaLoginHandler,
562+
}
563+
if self.token is None:
564+
identity_provider_kwargs["token"] = ""
565+
566+
self.identity_provider = self.identity_provider_class(
567+
**identity_provider_kwargs
568+
)
493569

494-
self.app = tornado.web.Application(
570+
self.authorizer = self.authorizer_class(
571+
parent=self, log=self.log, identity_provider=self.identity_provider
572+
)
573+
574+
settings = dict(
495575
base_url=self.base_url,
496576
server_url=self.server_url or self.base_url,
497577
kernel_manager=self.kernel_manager,
498578
kernel_spec_manager=self.kernel_spec_manager,
499579
allow_remote_access=True,
500580
autoreload=self.autoreload,
501581
voila_jinja2_env=env,
502-
jinja2_env=env,
582+
jinja2_env=server_env,
503583
static_path="/",
504584
server_root_dir="/",
505585
contents_manager=self.contents_manager,
506586
config_manager=self.config_manager,
587+
cookie_secret=self.cookie_secret,
588+
authorizer=self.authorizer,
589+
identity_provider=self.identity_provider,
590+
kernel_websocket_connection_class=self.kernel_websocket_connection_class,
591+
login_url=url_path_join(self.base_url, "/login"),
507592
)
508593

509-
self.app.settings.update(self.tornado_settings)
594+
return settings
510595

596+
def init_handlers(self) -> List:
597+
"""Initialize handlers for Voila application."""
511598
handlers = []
512599

513600
handlers.extend(
@@ -522,7 +609,7 @@ def start(self):
522609
url_path_join(
523610
self.server_url, r"/api/kernels/%s/channels" % _kernel_id_regex
524611
),
525-
ZMQChannelsHandler,
612+
KernelWebsocketHandler,
526613
),
527614
(
528615
url_path_join(self.server_url, r"/voila/templates/(.*)"),
@@ -549,8 +636,8 @@ def start(self):
549636
),
550637
]
551638
)
552-
553-
if preheat_kernel:
639+
handlers.extend(self.identity_provider.get_handlers())
640+
if self.voila_configuration.preheat_kernel:
554641
handlers.append(
555642
(
556643
url_path_join(
@@ -621,13 +708,30 @@ def start(self):
621708
),
622709
]
623710
)
711+
return handlers
712+
713+
def start(self):
714+
self.connection_dir = tempfile.mkdtemp(
715+
prefix="voila_", dir=self.connection_dir_root
716+
)
717+
self.log.info("Storing connection files in %s." % self.connection_dir)
718+
self.log.info("Serving static files from %s." % self.static_root)
719+
720+
settings = self.init_settings()
624721

722+
self.app = tornado.web.Application(**settings)
723+
self.app.settings.update(self.tornado_settings)
724+
handlers = self.init_handlers()
625725
self.app.add_handlers(".*$", handlers)
626726
self.listen()
627727

728+
def _handle_signal_stop(self, sig, frame):
729+
self.log.info("Handle signal %s." % sig)
730+
self.ioloop.add_callback_from_signal(self.ioloop.stop)
731+
628732
def stop(self):
629733
shutil.rmtree(self.connection_dir)
630-
run_sync(self.kernel_manager.shutdown_all())
734+
run_sync(self.kernel_manager.shutdown_all)()
631735

632736
def random_ports(self, port, n):
633737
"""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)