mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e7689f277 | ||
![]() |
b677655572 | ||
![]() |
9adc871448 | ||
![]() |
29d6540333 | ||
![]() |
5a4949faa5 | ||
![]() |
f2ab23b376 | ||
![]() |
b61582420a | ||
![]() |
f11ae34b73 | ||
![]() |
e91ab50d1b | ||
![]() |
4cb3a45ce4 | ||
![]() |
4e8f9b4334 | ||
![]() |
6131f2dbaa | ||
![]() |
a9dc588454 | ||
![]() |
537b2eaff6 | ||
![]() |
7f8a981aed | ||
![]() |
bc86e4c8f5 |
@@ -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: 4.1.0.dev
|
version: 4.0.2
|
||||||
servers:
|
servers:
|
||||||
- url: /hub/api
|
- url: /hub/api
|
||||||
security:
|
security:
|
||||||
|
@@ -45,7 +45,7 @@ additional packages.
|
|||||||
|
|
||||||
## Configuring Jupyter and IPython
|
## Configuring Jupyter and IPython
|
||||||
|
|
||||||
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)
|
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/configuring/config_overview.html)
|
||||||
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
||||||
have their own configuration systems.
|
have their own configuration systems.
|
||||||
|
|
||||||
@@ -212,13 +212,31 @@ By default, the single-user server launches JupyterLab,
|
|||||||
which is based on [Jupyter Server][].
|
which is based on [Jupyter Server][].
|
||||||
|
|
||||||
This is the default server when running JupyterHub ≥ 2.0.
|
This is the default server when running JupyterHub ≥ 2.0.
|
||||||
To switch to using the legacy Jupyter Notebook server, you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
To switch to using the legacy Jupyter Notebook server (notebook < 7.0), you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
||||||
(in the single-user environment) to:
|
(in the single-user environment) to:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::{note}
|
||||||
|
|
||||||
|
```
|
||||||
|
JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||||
|
```
|
||||||
|
|
||||||
|
is only valid for notebook < 7. notebook v7 is based on jupyter-server,
|
||||||
|
and the default jupyter-server application must be used.
|
||||||
|
Selecting the new notebook UI is no longer a matter of selecting the server app to launch,
|
||||||
|
but only the default URL for users to visit.
|
||||||
|
To use notebook v7 with JupyterHub, leave the default singleuser app config alone (or specify `JUPYTERHUB_SINGLEUSER_APP=jupyter-server`) and set the default _URL_ for user servers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Spawner.default_url = '/tree/'
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
[jupyter server]: https://jupyter-server.readthedocs.io
|
[jupyter server]: https://jupyter-server.readthedocs.io
|
||||||
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
||||||
|
|
||||||
|
@@ -10,6 +10,34 @@ command line for details.
|
|||||||
|
|
||||||
## 4.0
|
## 4.0
|
||||||
|
|
||||||
|
### 4.0.2 - 2023-08-10
|
||||||
|
|
||||||
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.1...4.0.2))
|
||||||
|
|
||||||
|
#### Enhancements made
|
||||||
|
|
||||||
|
- avoid counting failed requests to not-running servers as 'activity' [#4491](https://github.com/jupyterhub/jupyterhub/pull/4491) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
- improve permission-denied errors for various cases [#4489](https://github.com/jupyterhub/jupyterhub/pull/4489) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||||
|
|
||||||
|
#### Bugs fixed
|
||||||
|
|
||||||
|
- set root_dir when using singleuser extension [#4503](https://github.com/jupyterhub/jupyterhub/pull/4503) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
||||||
|
- Allow setting custom log_function in tornado_settings in SingleUserServer [#4475](https://github.com/jupyterhub/jupyterhub/pull/4475) ([@grios-stratio](https://github.com/grios-stratio), [@minrk](https://github.com/minrk))
|
||||||
|
|
||||||
|
#### Documentation improvements
|
||||||
|
|
||||||
|
- doc: update notebook config URL [#4523](https://github.com/jupyterhub/jupyterhub/pull/4523) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
|
- document how to use notebook v7 with jupyterhub [#4522](https://github.com/jupyterhub/jupyterhub/pull/4522) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||||
|
|
||||||
|
#### Contributors to this release
|
||||||
|
|
||||||
|
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||||
|
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||||
|
|
||||||
|
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2023-06-08&to=2023-08-10&type=c))
|
||||||
|
|
||||||
|
@agelosnm ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aagelosnm+updated%3A2023-06-08..2023-08-10&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @diocas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adiocas+updated%3A2023-06-08..2023-08-10&type=Issues)) | @grios-stratio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agrios-stratio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jhgoebbert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajhgoebbert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jtpio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajtpio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kosmonavtus ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akosmonavtus+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kreuzert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akreuzert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2023-06-08..2023-08-10&type=Issues)) | @martinRenou ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AmartinRenou+updated%3A2023-06-08..2023-08-10&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2023-06-08..2023-08-10&type=Issues)) | @opoplawski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aopoplawski+updated%3A2023-06-08..2023-08-10&type=Issues)) | @Ph0tonic ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3APh0tonic+updated%3A2023-06-08..2023-08-10&type=Issues)) | @sgaist ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgaist+updated%3A2023-06-08..2023-08-10&type=Issues)) | @trungleduc ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atrungleduc+updated%3A2023-06-08..2023-08-10&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2023-06-08..2023-08-10&type=Issues))
|
||||||
|
|
||||||
### 4.0.1 - 2023-06-08
|
### 4.0.1 - 2023-06-08
|
||||||
|
|
||||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.0...4.0.1))
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.0...4.0.1))
|
||||||
|
@@ -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 = (4, 1, 0, "", "dev")
|
version_info = (4, 0, 2, "", "")
|
||||||
|
|
||||||
# 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
|
||||||
|
@@ -89,6 +89,11 @@ class APIHandler(BaseHandler):
|
|||||||
if not hasattr(self, '_jupyterhub_user'):
|
if not hasattr(self, '_jupyterhub_user'):
|
||||||
# called too early to check if we're token-authenticated
|
# called too early to check if we're token-authenticated
|
||||||
return
|
return
|
||||||
|
if self._jupyterhub_user is None and 'Origin' not in self.request.headers:
|
||||||
|
# don't raise xsrf if auth failed
|
||||||
|
# don't apply this shortcut to actual cross-site requests, which have an 'Origin' header,
|
||||||
|
# which would reveal if there are credentials present
|
||||||
|
return
|
||||||
if getattr(self, '_token_authenticated', False):
|
if getattr(self, '_token_authenticated', False):
|
||||||
# if token-authenticated, ignore XSRF
|
# if token-authenticated, ignore XSRF
|
||||||
return
|
return
|
||||||
|
@@ -236,11 +236,13 @@ class BaseHandler(RequestHandler):
|
|||||||
def check_xsrf_cookie(self):
|
def check_xsrf_cookie(self):
|
||||||
try:
|
try:
|
||||||
return super().check_xsrf_cookie()
|
return super().check_xsrf_cookie()
|
||||||
except Exception as e:
|
except web.HTTPError as e:
|
||||||
# ensure _juptyerhub_user is defined on rejected requests
|
# ensure _jupyterhub_user is defined on rejected requests
|
||||||
if not hasattr(self, "_jupyterhub_user"):
|
if not hasattr(self, "_jupyterhub_user"):
|
||||||
self._jupyterhub_user = None
|
self._jupyterhub_user = None
|
||||||
self._resolve_roles_and_scopes()
|
self._resolve_roles_and_scopes()
|
||||||
|
# rewrite message because we use this on methods other than POST
|
||||||
|
e.log_message = e.log_message.replace("POST", self.request.method)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1431,6 +1433,12 @@ class UserUrlHandler(BaseHandler):
|
|||||||
# accept token auth for API requests that are probably to non-running servers
|
# accept token auth for API requests that are probably to non-running servers
|
||||||
_accept_token_auth = True
|
_accept_token_auth = True
|
||||||
|
|
||||||
|
# don't consider these redirects 'activity'
|
||||||
|
# if the redirect is followed and the subsequent action taken,
|
||||||
|
# _that_ is activity
|
||||||
|
def _record_activity(self, obj, timestamp=None):
|
||||||
|
return False
|
||||||
|
|
||||||
def _fail_api_request(self, user_name='', server_name=''):
|
def _fail_api_request(self, user_name='', server_name=''):
|
||||||
"""Fail an API request to a not-running server"""
|
"""Fail an API request to a not-running server"""
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
@@ -845,6 +845,15 @@ def needs_scope(*scopes):
|
|||||||
def scope_decorator(func):
|
def scope_decorator(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def _auth_func(self, *args, **kwargs):
|
def _auth_func(self, *args, **kwargs):
|
||||||
|
if not self.current_user:
|
||||||
|
# not authenticated at all, fail with more generic message
|
||||||
|
# this is the most likely permission error - missing or mis-specified credentials,
|
||||||
|
# don't indicate that they have insufficient permissions.
|
||||||
|
raise web.HTTPError(
|
||||||
|
403,
|
||||||
|
"Missing or invalid credentials.",
|
||||||
|
)
|
||||||
|
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
bound_sig = sig.bind(self, *args, **kwargs)
|
bound_sig = sig.bind(self, *args, **kwargs)
|
||||||
bound_sig.apply_defaults()
|
bound_sig.apply_defaults()
|
||||||
@@ -853,6 +862,11 @@ def needs_scope(*scopes):
|
|||||||
self.expanded_scopes = {}
|
self.expanded_scopes = {}
|
||||||
self.parsed_scopes = {}
|
self.parsed_scopes = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_point = self.request.path
|
||||||
|
except AttributeError:
|
||||||
|
end_point = self.__name__
|
||||||
|
|
||||||
s_kwargs = {}
|
s_kwargs = {}
|
||||||
for resource in {'user', 'server', 'group', 'service'}:
|
for resource in {'user', 'server', 'group', 'service'}:
|
||||||
resource_name = resource + '_name'
|
resource_name = resource + '_name'
|
||||||
@@ -860,14 +874,10 @@ def needs_scope(*scopes):
|
|||||||
resource_value = bound_sig.arguments[resource_name]
|
resource_value = bound_sig.arguments[resource_name]
|
||||||
s_kwargs[resource] = resource_value
|
s_kwargs[resource] = resource_value
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
app_log.debug("Checking access via scope %s", scope)
|
app_log.debug("Checking access to %s via scope %s", end_point, scope)
|
||||||
has_access = _check_scope_access(self, scope, **s_kwargs)
|
has_access = _check_scope_access(self, scope, **s_kwargs)
|
||||||
if has_access:
|
if has_access:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
try:
|
|
||||||
end_point = self.request.path
|
|
||||||
except AttributeError:
|
|
||||||
end_point = self.__name__
|
|
||||||
app_log.warning(
|
app_log.warning(
|
||||||
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
|
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
|
||||||
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)
|
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
Default app changed to launch `jupyter labhub`.
|
Default app changed to launch `jupyter labhub`.
|
||||||
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
|
Use JUPYTERHUB_SINGLEUSER_APP='notebook' for the legacy 'classic' notebook server (requires notebook<7).
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -27,7 +27,25 @@ JUPYTERHUB_SINGLEUSER_APP = _app_shortcuts.get(
|
|||||||
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
|
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if JUPYTERHUB_SINGLEUSER_APP:
|
if JUPYTERHUB_SINGLEUSER_APP:
|
||||||
|
if JUPYTERHUB_SINGLEUSER_APP in {"notebook", _app_shortcuts["notebook"]}:
|
||||||
|
# better error for notebook v7, which uses jupyter-server
|
||||||
|
# when the legacy notebook server is requested
|
||||||
|
try:
|
||||||
|
from notebook import __version__
|
||||||
|
except ImportError:
|
||||||
|
# will raise later
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# check if this failed because of notebook v7
|
||||||
|
_notebook_major_version = int(__version__.split(".", 1)[0])
|
||||||
|
if _notebook_major_version >= 7:
|
||||||
|
raise ImportError(
|
||||||
|
f"JUPYTERHUB_SINGLEUSER_APP={JUPYTERHUB_SINGLEUSER_APP} is not valid with notebook>=7 (have notebook=={__version__}).\n"
|
||||||
|
f"Leave $JUPYTERHUB_SINGLEUSER_APP unspecified (or use the default JUPYTERHUB_SINGLEUSER_APP=jupyter-server), "
|
||||||
|
'and set `c.Spawner.default_url = "/tree"` to make notebook v7 the default UI.'
|
||||||
|
)
|
||||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||||
else:
|
else:
|
||||||
App = None
|
App = None
|
||||||
|
@@ -483,6 +483,11 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
cfg.answer_yes = True
|
cfg.answer_yes = True
|
||||||
self.config.FileContentsManager.delete_to_trash = False
|
self.config.FileContentsManager.delete_to_trash = False
|
||||||
|
|
||||||
|
# load Spawner.notebook_dir configuration, if given
|
||||||
|
root_dir = os.getenv("JUPYTERHUB_ROOT_DIR", None)
|
||||||
|
if root_dir:
|
||||||
|
cfg.root_dir = os.path.expanduser(root_dir)
|
||||||
|
|
||||||
# load http server config from environment
|
# load http server config from environment
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||||
if url.port:
|
if url.port:
|
||||||
@@ -617,7 +622,9 @@ class JupyterHubSingleUser(ExtensionApp):
|
|||||||
app.web_app.settings[
|
app.web_app.settings[
|
||||||
"page_config_hook"
|
"page_config_hook"
|
||||||
] = app.identity_provider.page_config_hook
|
] = app.identity_provider.page_config_hook
|
||||||
app.web_app.settings["log_function"] = log_request
|
# if the user has configured a log function in the tornado settings, do not override it
|
||||||
|
if not 'log_function' in app.config.ServerApp.get('tornado_settings', {}):
|
||||||
|
app.web_app.settings["log_function"] = log_request
|
||||||
# add jupyterhub version header
|
# add jupyterhub version header
|
||||||
headers = app.web_app.settings.setdefault("headers", {})
|
headers = app.web_app.settings.setdefault("headers", {})
|
||||||
headers["X-JupyterHub-Version"] = __version__
|
headers["X-JupyterHub-Version"] = __version__
|
||||||
|
@@ -669,7 +669,8 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
# load the hub-related settings into the tornado settings dict
|
# load the hub-related settings into the tornado settings dict
|
||||||
self.init_hub_auth()
|
self.init_hub_auth()
|
||||||
s = self.tornado_settings
|
s = self.tornado_settings
|
||||||
s['log_function'] = log_request
|
# if the user has configured a log function in the tornado settings, do not override it
|
||||||
|
s.setdefault('log_function', log_request)
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
s['group'] = self.group
|
s['group'] = self.group
|
||||||
s['hub_prefix'] = self.hub_prefix
|
s['hub_prefix'] = self.hub_prefix
|
||||||
|
@@ -30,6 +30,7 @@ class JupyterHubTestHandler(JupyterHandler):
|
|||||||
info = {
|
info = {
|
||||||
"current_user": self.current_user,
|
"current_user": self.current_user,
|
||||||
"config": self.app.config,
|
"config": self.app.config,
|
||||||
|
"root_dir": self.contents_manager.root_dir,
|
||||||
"disable_user_config": getattr(self.app, "disable_user_config", None),
|
"disable_user_config": getattr(self.app, "disable_user_config", None),
|
||||||
"settings": self.settings,
|
"settings": self.settings,
|
||||||
"config_file_paths": self.app.config_file_paths,
|
"config_file_paths": self.app.config_file_paths,
|
||||||
|
@@ -122,6 +122,41 @@ async def test_xsrf_check(app, username, method, path, xsrf_in_url):
|
|||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize(
|
||||||
|
"auth, expected_message",
|
||||||
|
[
|
||||||
|
("", "Missing or invalid credentials"),
|
||||||
|
("cookie_no_xsrf", "'_xsrf' argument missing from GET"),
|
||||||
|
("cookie_xsrf_mismatch", "XSRF cookie does not match GET argument"),
|
||||||
|
("token_no_scope", "requires any of [list:users]"),
|
||||||
|
("cookie_no_scope", "requires any of [list:users]"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_permission_error_messages(app, user, auth, expected_message):
|
||||||
|
# 1. no credentials, should be 403 and not mention xsrf
|
||||||
|
|
||||||
|
url = public_url(app, path="hub/api/users")
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
kwargs["headers"] = headers = {}
|
||||||
|
kwargs["params"] = params = {}
|
||||||
|
if auth == "token_no_scope":
|
||||||
|
token = user.new_api_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
elif "cookie" in auth:
|
||||||
|
cookies = kwargs["cookies"] = await app.login_user(user.name)
|
||||||
|
if auth == "cookie_no_scope":
|
||||||
|
params["_xsrf"] = cookies["_xsrf"]
|
||||||
|
if auth == "cookie_xsrf_mismatch":
|
||||||
|
params["_xsrf"] = "somethingelse"
|
||||||
|
|
||||||
|
r = await async_requests.get(url, **kwargs)
|
||||||
|
assert r.status_code == 403
|
||||||
|
response = r.json()
|
||||||
|
message = response["message"]
|
||||||
|
assert expected_message in message
|
||||||
|
|
||||||
|
|
||||||
# --------------
|
# --------------
|
||||||
# User API tests
|
# User API tests
|
||||||
# --------------
|
# --------------
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
from pprint import pprint
|
||||||
from subprocess import CalledProcessError, check_output
|
from subprocess import CalledProcessError, check_output
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
@@ -171,9 +172,7 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
|||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
info = r.json()
|
info = r.json()
|
||||||
import pprint
|
pprint(info)
|
||||||
|
|
||||||
pprint.pprint(info)
|
|
||||||
assert info['disable_user_config']
|
assert info['disable_user_config']
|
||||||
server_config = info['config']
|
server_config = info['config']
|
||||||
settings = info['settings']
|
settings = info['settings']
|
||||||
@@ -198,6 +197,79 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
|||||||
assert_not_in_home(path, key)
|
assert_not_in_home(path, key)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("extension", [True, False])
|
||||||
|
@pytest.mark.parametrize("notebook_dir", ["", "~", "~/sub", "ABS"])
|
||||||
|
async def test_notebook_dir(
|
||||||
|
request, app, tmpdir, user, full_spawn, extension, notebook_dir
|
||||||
|
):
|
||||||
|
if extension:
|
||||||
|
try:
|
||||||
|
import jupyter_server # noqa
|
||||||
|
except ImportError:
|
||||||
|
pytest.skip("needs jupyter-server 2")
|
||||||
|
else:
|
||||||
|
if jupyter_server.version_info < (2,):
|
||||||
|
pytest.skip("needs jupyter-server 2")
|
||||||
|
|
||||||
|
token = user.new_api_token(scopes=["access:servers!user"])
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
spawner = user.spawner
|
||||||
|
if extension:
|
||||||
|
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "1"
|
||||||
|
else:
|
||||||
|
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "0"
|
||||||
|
|
||||||
|
home_dir = tmpdir.join("home").mkdir()
|
||||||
|
sub_dir = home_dir.join("sub").mkdir()
|
||||||
|
with sub_dir.join("subfile.txt").open("w") as f:
|
||||||
|
f.write("txt\n")
|
||||||
|
abs_dir = tmpdir.join("abs").mkdir()
|
||||||
|
with abs_dir.join("absfile.txt").open("w") as f:
|
||||||
|
f.write("absfile\n")
|
||||||
|
|
||||||
|
if notebook_dir:
|
||||||
|
expected_root_dir = notebook_dir.replace("ABS", str(abs_dir)).replace(
|
||||||
|
"~", str(home_dir)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
expected_root_dir = str(home_dir)
|
||||||
|
|
||||||
|
spawner.notebook_dir = notebook_dir.replace("ABS", str(abs_dir))
|
||||||
|
|
||||||
|
# home_dir is defined on SimpleSpawner
|
||||||
|
user.spawner.home_dir = home = str(home_dir)
|
||||||
|
spawner.environment["HOME"] = home
|
||||||
|
await user.spawn()
|
||||||
|
await app.proxy.add_user(user)
|
||||||
|
url = public_url(app, user)
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_path_join(public_url(app, user), 'jupyterhub-test-info'), headers=headers
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
info = r.json()
|
||||||
|
pprint(info)
|
||||||
|
|
||||||
|
assert info["root_dir"] == expected_root_dir
|
||||||
|
# secondary check: make sure it has the intended effect on root_dir
|
||||||
|
r = await async_requests.get(
|
||||||
|
url_path_join(public_url(app, user), 'api/contents/'), headers=headers
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
root_contents = sorted(item['name'] for item in r.json()['content'])
|
||||||
|
|
||||||
|
# check contents
|
||||||
|
if not notebook_dir or notebook_dir == "~":
|
||||||
|
# use any to avoid counting possible automatically created files in $HOME
|
||||||
|
assert 'sub' in root_contents
|
||||||
|
elif notebook_dir == "ABS":
|
||||||
|
assert 'absfile.txt' in root_contents
|
||||||
|
elif notebook_dir == "~/sub":
|
||||||
|
assert 'subfile.txt' in root_contents
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No contents check for {notebook_dir}")
|
||||||
|
|
||||||
|
|
||||||
def test_help_output():
|
def test_help_output():
|
||||||
out = check_output(
|
out = check_output(
|
||||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
||||||
|
@@ -43,7 +43,7 @@ target_version = [
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "4.1.0.dev"
|
current = "4.0.2"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
Reference in New Issue
Block a user