Compare commits

...

23 Commits
2.3.0 ... 2.x

Author SHA1 Message Date
Yuvi Panda
220eb87bce Merge pull request #3984 from meeseeksmachine/auto-backport-of-pr-3936-on-2.x
Backport PR #3936 on branch 2.x (admin: Hub is responsible for username validation)
2022-07-29 10:43:28 -07:00
Erik Sundell
f9e9150abc Merge pull request #3993 from minrk/2.x
backport nbclassic fixes to 2.x
2022-07-29 15:32:13 +02:00
Min RK
8074469ad7 Backport PR #3977: unpin nbclassic
0.4.3 is out, see if it fixes things

Signed-off-by: Min RK <benjaminrk@gmail.com>
2022-07-29 15:11:00 +02:00
Min RK
46d2455aff Backport PR #3971: nbclassic extension name has been renamed
ref: https://github.com/jupyter/nbclassic/pull/96/files diff-baf53b9a1d8f038c7de824e4928d10356271b26bacf19ffccba98454e685438eL109-R110

our patches to the jinja env need updating to find the new env. Until then, nbclassic 0.4.x will not get the template patches (the 'control panel' link)

Signed-off-by: Min RK <benjaminrk@gmail.com>
2022-07-29 15:09:08 +02:00
YuviPanda
72e4119e1a Commit built js 2022-07-18 19:18:38 -05:00
Erik Sundell
faa1754645 Backport PR #3936: admin: Hub is responsible for username validation 2022-07-19 00:13:54 +00:00
Erik Sundell
318f739ba9 Bump to 2.3.2.dev 2022-06-06 16:26:41 +02:00
Erik Sundell
20b3229249 Bump to 2.3.1 2022-06-06 16:26:12 +02:00
Yuvi Panda
f0862f1d10 Merge pull request #3930 from consideRatio/pr/add-changelog-to-2x-branch
Add changelog for 2.3.1
2022-06-06 19:53:45 +05:30
Erik Sundell
3c5f9b255e Add changelog for 2.3.1 2022-06-06 16:15:36 +02:00
Erik Sundell
b6d9d5c120 Merge pull request #3926 from meeseeksmachine/auto-backport-of-pr-3910-on-2.x
Backport PR #3910 on branch 2.x (use equality to filter token prefixes)
2022-06-06 15:36:36 +02:00
Yuvi Panda
bccd0e2ff1 Merge pull request #3928 from yuvipanda/auto-backport-of-pr-3919-on-2.x
Auto backport of pr 3919 on 2.x
2022-06-06 18:40:16 +05:30
Yuvi Panda
a2d39c693d Merge pull request #3927 from meeseeksmachine/auto-backport-of-pr-3918-on-2.x
Backport PR #3918 on branch 2.x (set default_url via config)
2022-06-06 18:40:06 +05:30
Yuvi Panda
76e65da9ff Merge pull request #3925 from meeseeksmachine/auto-backport-of-pr-3906-on-2.x
Backport PR #3906 on branch 2.x (Force add existing certificates)
2022-06-06 18:39:24 +05:30
Yuvi Panda
eb9bb71655 Merge pull request #3924 from meeseeksmachine/auto-backport-of-pr-3889-on-2.x
Backport PR #3889 on branch 2.x (admin: make user-info table selectable)
2022-06-06 18:39:12 +05:30
Yuvi Panda
a39ef8f163 Merge pull request #3923 from meeseeksmachine/auto-backport-of-pr-3837-on-2.x
Backport PR #3837 on branch 2.x (ensure _import_error is set when JUPYTERHUB_SINGLEUSER_APP is unavailable)
2022-06-06 18:38:57 +05:30
Yuvi Panda
f4727cba47 Backport PR #3919: ensure custom template is loaded with jupyter-server notebook extension 2022-06-03 21:15:44 +05:30
Yuvi Panda
14dfa65c75 Backport PR #3918: set default_url via config 2022-06-03 15:17:09 +00:00
Yuvi Panda
9f23bc2959 Backport PR #3910: use equality to filter token prefixes 2022-06-03 15:17:01 +00:00
Min RK
24e8362401 Backport PR #3906: Force add existing certificates 2022-06-03 15:16:46 +00:00
Min RK
c4c662843c Backport PR #3889: admin: make user-info table selectable 2022-06-03 15:16:29 +00:00
Erik Sundell
6d5b13962c Backport PR #3837: ensure _import_error is set when JUPYTERHUB_SINGLEUSER_APP is unavailable 2022-06-03 15:16:20 +00:00
Min RK
fe64595d75 Bump to 2.3.1.dev 2022-05-06 16:06:06 +02:00
15 changed files with 133 additions and 64 deletions

View File

@@ -9,6 +9,10 @@ cryptography
html5lib # needed for beautifulsoup html5lib # needed for beautifulsoup
jupyterlab >=3 jupyterlab >=3
mock mock
# nbclassic provides the '/tree/' handler, which we use in tests
# it is a transitive dependency via jupyterlab,
# but depend on it directly
nbclassic
pre-commit pre-commit
pytest>=3.3 pytest>=3.3
pytest-asyncio; python_version < "3.7" pytest-asyncio; python_version < "3.7"

View File

@@ -6,7 +6,7 @@ info:
description: The REST API for JupyterHub description: The REST API for JupyterHub
license: license:
name: BSD-3-Clause name: BSD-3-Clause
version: 2.3.0 version: 2.3.2.dev
servers: servers:
- url: /hub/api - url: /hub/api
security: security:

File diff suppressed because one or more lines are too long

View File

@@ -60,7 +60,10 @@ const AddUser = (props) => {
placeholder="usernames separated by line" placeholder="usernames separated by line"
data-testid="user-textarea" data-testid="user-textarea"
onBlur={(e) => { onBlur={(e) => {
let split_users = e.target.value.split("\n"); let split_users = e.target.value
.split("\n")
.map((u) => u.trim())
.filter((u) => u.length > 0);
setUsers(split_users); setUsers(split_users);
}} }}
></textarea> ></textarea>
@@ -88,17 +91,7 @@ const AddUser = (props) => {
data-testid="submit" data-testid="submit"
className="btn btn-primary" className="btn btn-primary"
onClick={() => { onClick={() => {
let filtered_users = users.filter( addUsers(users, admin)
(e) =>
e.length > 2 &&
/[!@#$%^&*(),.?":{}|<>]/g.test(e) == false
);
if (filtered_users.length < users.length) {
setUsers(filtered_users);
failRegexEvent();
}
addUsers(filtered_users, admin)
.then((data) => .then((data) =>
data.status < 300 data.status < 300
? updateUsers(0, limit) ? updateUsers(0, limit)

View File

@@ -70,12 +70,12 @@ test("Removes users when they fail Regex", async () => {
let textarea = screen.getByTestId("user-textarea"); let textarea = screen.getByTestId("user-textarea");
let submit = screen.getByTestId("submit"); let submit = screen.getByTestId("submit");
fireEvent.blur(textarea, { target: { value: "foo\nbar\n!!*&*" } }); fireEvent.blur(textarea, { target: { value: "foo \n bar\na@b.co\n \n\n" } });
await act(async () => { await act(async () => {
fireEvent.click(submit); fireEvent.click(submit);
}); });
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar"], false); expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar", "a@b.co"], false);
}); });
test("Correctly submits admin", async () => { test("Correctly submits admin", async () => {

View File

@@ -59,7 +59,7 @@ const CreateGroup = (props) => {
value={groupName} value={groupName}
placeholder="group name..." placeholder="group name..."
onChange={(e) => { onChange={(e) => {
setGroupName(e.target.value); setGroupName(e.target.value.trim());
}} }}
></input> ></input>
</div> </div>

View File

@@ -200,6 +200,25 @@ const ServerDashboard = (props) => {
); );
}; };
const ServerRowTable = ({ data }) => {
return (
<ReactObjectTableViewer
className="table-striped table-bordered"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={data}
/>
);
};
const serverRow = (user, server) => { const serverRow = (user, server) => {
const { servers, ...userNoServers } = user; const { servers, ...userNoServers } = user;
const serverNameDash = server.name ? `-${server.name}` : ""; const serverNameDash = server.name ? `-${server.name}` : "";
@@ -286,37 +305,11 @@ const ServerDashboard = (props) => {
> >
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}> <Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>User</Card.Title> <Card.Title>User</Card.Title>
<ReactObjectTableViewer <ServerRowTable data={userNoServers} />
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={userNoServers}
/>
</Card> </Card>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}> <Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>Server</Card.Title> <Card.Title>Server</Card.Title>
<ReactObjectTableViewer <ServerRowTable data={server} />
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={server}
/>
</Card> </Card>
</CardGroup> </CardGroup>
</Collapse> </Collapse>

View File

@@ -2,7 +2,7 @@
# 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.
# version_info updated by running `tbump` # version_info updated by running `tbump`
version_info = (2, 3, 0, "", "") version_info = (2, 3, 2, "", "dev")
# pep 440 version: no dot before beta/rc, but before .dev # pep 440 version: no dot before beta/rc, but before .dev
# 0.1.0rc1 # 0.1.0rc1

View File

@@ -1689,7 +1689,9 @@ class JupyterHub(Application):
for authority, files in self.internal_ssl_authorities.items(): for authority, files in self.internal_ssl_authorities.items():
if files: if files:
self.log.info("Adding CA for %s", authority) self.log.info("Adding CA for %s", authority)
certipy.store.add_record(authority, is_ca=True, files=files) certipy.store.add_record(
authority, is_ca=True, files=files, overwrite=True
)
self.internal_trust_bundles = certipy.trust_from_graph( self.internal_trust_bundles = certipy.trust_from_graph(
self.internal_ssl_components_trust self.internal_ssl_components_trust

View File

@@ -536,9 +536,7 @@ class Hashed(Expiring):
prefix = token[: cls.prefix_length] prefix = token[: cls.prefix_length]
# since we can't filter on hashed values, filter on prefix # since we can't filter on hashed values, filter on prefix
# so we aren't comparing with all tokens # so we aren't comparing with all tokens
prefix_match = db.query(cls).filter( prefix_match = db.query(cls).filter_by(prefix=prefix)
bindparam('prefix', prefix).startswith(cls.prefix)
)
prefix_match = prefix_match.filter( prefix_match = prefix_match.filter(
or_(cls.expires_at == None, cls.expires_at >= cls.now()) or_(cls.expires_at == None, cls.expires_at >= cls.now())
) )

View File

@@ -29,9 +29,9 @@ else:
try: try:
App = import_item(JUPYTERHUB_SINGLEUSER_APP) App = import_item(JUPYTERHUB_SINGLEUSER_APP)
except ImportError as e: except ImportError as e:
continue
if _import_error is None: if _import_error is None:
_import_error = e _import_error = e
continue
else: else:
break break
if App is None: if App is None:

View File

@@ -182,6 +182,7 @@ page_template = """
<span> <span>
<a href='{{hub_control_panel_url}}' <a href='{{hub_control_panel_url}}'
id='jupyterhub-control-panel-link'
class='btn btn-default btn-sm navbar-btn pull-right' class='btn btn-default btn-sm navbar-btn pull-right'
style='margin-right: 4px; margin-left: 2px;'> style='margin-right: 4px; margin-left: 2px;'>
Control Panel Control Panel
@@ -633,8 +634,15 @@ class SingleUserNotebookAppMixin(Configurable):
# disable trash by default # disable trash by default
# this can be re-enabled by config # this can be re-enabled by config
self.config.FileContentsManager.delete_to_trash = False self.config.FileContentsManager.delete_to_trash = False
# load default-url env at higher priority than `@default`,
# which may have their own _defaults_ which should not override explicit default_url config
# via e.g. c.Spawner.default_url. Seen in jupyterlab's SingleUserLabApp.
default_url = os.environ.get("JUPYTERHUB_DEFAULT_URL")
if default_url:
self.config[self.__class__.__name__].default_url = default_url
self._log_app_versions() self._log_app_versions()
return super().initialize(argv) super().initialize(argv)
self.patch_templates()
def start(self): def start(self):
self.log.info("Starting jupyterhub-singleuser server version %s", __version__) self.log.info("Starting jupyterhub-singleuser server version %s", __version__)
@@ -705,7 +713,6 @@ class SingleUserNotebookAppMixin(Configurable):
# apply X-JupyterHub-Version to *all* request handlers (even redirects) # apply X-JupyterHub-Version to *all* request handlers (even redirects)
self.patch_default_headers() self.patch_default_headers()
self.patch_templates()
def page_config_hook(self, handler, page_config): def page_config_hook(self, handler, page_config):
"""JupyterLab page config hook """JupyterLab page config hook
@@ -738,19 +745,32 @@ class SingleUserNotebookAppMixin(Configurable):
) )
self.jinja_template_vars['hub_host'] = self.hub_host self.jinja_template_vars['hub_host'] = self.hub_host
self.jinja_template_vars['hub_prefix'] = self.hub_prefix self.jinja_template_vars['hub_prefix'] = self.hub_prefix
env = self.web_app.settings['jinja2_env'] self.jinja_template_vars[
'hub_control_panel_url'
] = self.hub_host + url_path_join(self.hub_prefix, 'home')
env.globals['hub_control_panel_url'] = self.hub_host + url_path_join( settings = self.web_app.settings
self.hub_prefix, 'home' # patch classic notebook jinja env
) jinja_envs = []
if 'jinja2_env' in settings:
# default jinja env (should we do this on jupyter-server, or only notebook?)
jinja_envs.append(settings['jinja2_env'])
for ext_name in ("notebook", "nbclassic"):
env_name = f"{ext_name}_jinja2_env"
if env_name in settings:
# when running with jupyter-server, classic notebook (nbclassic server extension or notebook v7)
# gets its own jinja env, which needs the same patch
jinja_envs.append(settings[env_name])
# patch jinja env loading to modify page template # patch jinja env loading to get modified template, only for base page.html
def get_page(name): def get_page(name):
if name == 'page.html': if name == 'page.html':
return page_template return page_template
orig_loader = env.loader for jinja_env in jinja_envs:
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader]) jinja_env.loader = ChoiceLoader(
[FunctionLoader(get_page), jinja_env.loader]
)
def load_server_extensions(self): def load_server_extensions(self):
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect # Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect

View File

@@ -5,9 +5,11 @@ from contextlib import contextmanager
from subprocess import CalledProcessError from subprocess import CalledProcessError
from subprocess import check_output from subprocess import check_output
from unittest import mock from unittest import mock
from urllib.parse import urlencode
from urllib.parse import urlparse from urllib.parse import urlparse
import pytest import pytest
from bs4 import BeautifulSoup
import jupyterhub import jupyterhub
from .. import orm from .. import orm
@@ -16,6 +18,7 @@ from .mocking import public_url
from .mocking import StubSingleUserSpawner from .mocking import StubSingleUserSpawner
from .utils import async_requests from .utils import async_requests
from .utils import AsyncSession from .utils import AsyncSession
from .utils import get_page
@contextmanager @contextmanager
@@ -196,10 +199,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
import jupyter_server # noqa import jupyter_server # noqa
except ImportError: except ImportError:
have_server = False have_server = False
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
else: else:
have_server = True have_server = True
expect_error = False try:
import notebook.notebookapp # noqa
except ImportError:
have_notebook = False
else:
have_notebook = True
if JUPYTERHUB_SINGLEUSER_APP.startswith("notebook."):
expect_error = not have_notebook
elif JUPYTERHUB_SINGLEUSER_APP.startswith("jupyter_server."):
expect_error = not have_server
else:
# not specified, will try both
expect_error = not (have_server or have_notebook)
if expect_error: if expect_error:
ctx = pytest.raises(CalledProcessError) ctx = pytest.raises(CalledProcessError)
@@ -225,3 +240,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
else: else:
assert '--ServerApp.' in out assert '--ServerApp.' in out
assert '--NotebookApp.' not in out assert '--NotebookApp.' not in out
async def test_nbclassic_control_panel(app, user):
# use StubSingleUserSpawner to launch a single-user app in a thread
app.spawner_class = StubSingleUserSpawner
app.tornado_settings['spawner_class'] = StubSingleUserSpawner
# login, start the server
await user.spawn()
cookies = await app.login_user(user.name)
next_url = url_path_join(user.url, "tree/")
url = '/?' + urlencode({'next': next_url})
r = await get_page(url, app, cookies=cookies)
r.raise_for_status()
assert urlparse(r.url).path == urlparse(next_url).path
page = BeautifulSoup(r.text, "html.parser")
link = page.find("a", id="jupyterhub-control-panel-link")
assert link, f"Missing jupyterhub-control-panel-link in {page}"
assert link["href"] == url_path_join(app.base_url, "hub/home")

View File

@@ -15,7 +15,7 @@ target_version = [
github_url = "https://github.com/jupyterhub/jupyterhub" github_url = "https://github.com/jupyterhub/jupyterhub"
[tool.tbump.version] [tool.tbump.version]
current = "2.3.0" current = "2.3.2.dev"
# Example of a semver regexp. # Example of a semver regexp.
# Make sure this matches current_version before # Make sure this matches current_version before

File diff suppressed because one or more lines are too long