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
jupyterlab >=3
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
pytest>=3.3
pytest-asyncio; python_version < "3.7"

View File

@@ -6,7 +6,7 @@ info:
description: The REST API for JupyterHub
license:
name: BSD-3-Clause
version: 2.3.0
version: 2.3.2.dev
servers:
- url: /hub/api
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"
data-testid="user-textarea"
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);
}}
></textarea>
@@ -88,17 +91,7 @@ const AddUser = (props) => {
data-testid="submit"
className="btn btn-primary"
onClick={() => {
let filtered_users = users.filter(
(e) =>
e.length > 2 &&
/[!@#$%^&*(),.?":{}|<>]/g.test(e) == false
);
if (filtered_users.length < users.length) {
setUsers(filtered_users);
failRegexEvent();
}
addUsers(filtered_users, admin)
addUsers(users, admin)
.then((data) =>
data.status < 300
? updateUsers(0, limit)

View File

@@ -70,12 +70,12 @@ test("Removes users when they fail Regex", async () => {
let textarea = screen.getByTestId("user-textarea");
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 () => {
fireEvent.click(submit);
});
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar"], false);
expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar", "a@b.co"], false);
});
test("Correctly submits admin", async () => {

View File

@@ -59,7 +59,7 @@ const CreateGroup = (props) => {
value={groupName}
placeholder="group name..."
onChange={(e) => {
setGroupName(e.target.value);
setGroupName(e.target.value.trim());
}}
></input>
</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 { servers, ...userNoServers } = user;
const serverNameDash = server.name ? `-${server.name}` : "";
@@ -286,37 +305,11 @@ const ServerDashboard = (props) => {
>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>User</Card.Title>
<ReactObjectTableViewer
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={userNoServers}
/>
<ServerRowTable data={userNoServers} />
</Card>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>Server</Card.Title>
<ReactObjectTableViewer
className="table-striped table-bordered admin-table-head"
style={{
padding: "3px 6px",
margin: "auto",
}}
keyStyle={{
padding: "4px",
}}
valueStyle={{
padding: "4px",
}}
data={server}
/>
<ServerRowTable data={server} />
</Card>
</CardGroup>
</Collapse>

View File

@@ -2,7 +2,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
# 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
# 0.1.0rc1

View File

@@ -1689,7 +1689,9 @@ class JupyterHub(Application):
for authority, files in self.internal_ssl_authorities.items():
if files:
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_ssl_components_trust

View File

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

View File

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

View File

@@ -182,6 +182,7 @@ page_template = """
<span>
<a href='{{hub_control_panel_url}}'
id='jupyterhub-control-panel-link'
class='btn btn-default btn-sm navbar-btn pull-right'
style='margin-right: 4px; margin-left: 2px;'>
Control Panel
@@ -633,8 +634,15 @@ class SingleUserNotebookAppMixin(Configurable):
# disable trash by default
# this can be re-enabled by config
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()
return super().initialize(argv)
super().initialize(argv)
self.patch_templates()
def start(self):
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)
self.patch_default_headers()
self.patch_templates()
def page_config_hook(self, handler, page_config):
"""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_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(
self.hub_prefix, 'home'
)
settings = self.web_app.settings
# 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):
if name == 'page.html':
return page_template
orig_loader = env.loader
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
for jinja_env in jinja_envs:
jinja_env.loader = ChoiceLoader(
[FunctionLoader(get_page), jinja_env.loader]
)
def load_server_extensions(self):
# 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 check_output
from unittest import mock
from urllib.parse import urlencode
from urllib.parse import urlparse
import pytest
from bs4 import BeautifulSoup
import jupyterhub
from .. import orm
@@ -16,6 +18,7 @@ from .mocking import public_url
from .mocking import StubSingleUserSpawner
from .utils import async_requests
from .utils import AsyncSession
from .utils import get_page
@contextmanager
@@ -196,10 +199,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
import jupyter_server # noqa
except ImportError:
have_server = False
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
else:
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:
ctx = pytest.raises(CalledProcessError)
@@ -225,3 +240,22 @@ def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
else:
assert '--ServerApp.' 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"
[tool.tbump.version]
current = "2.3.0"
current = "2.3.2.dev"
# Example of a semver regexp.
# Make sure this matches current_version before

File diff suppressed because one or more lines are too long