mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 18:44:10 +00:00
Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
61b0e8bef5 | ||
![]() |
64f3938528 | ||
![]() |
85bc92d88e | ||
![]() |
7bcda18564 | ||
![]() |
86da36857e | ||
![]() |
530833e930 | ||
![]() |
3b0850fa9b | ||
![]() |
1366911be6 | ||
![]() |
fe276eac64 | ||
![]() |
9209ccd0de | ||
![]() |
3b2a1a37f9 | ||
![]() |
6007ba78b0 | ||
![]() |
9cb19cc342 | ||
![]() |
0f471f4e12 | ||
![]() |
68db740998 | ||
![]() |
9c0c6f25b7 | ||
![]() |
5f0077cb5b | ||
![]() |
a6a2056cca | ||
![]() |
fb1e81212f | ||
![]() |
17f811d0b4 | ||
![]() |
34398d94de | ||
![]() |
6bf94fde48 | ||
![]() |
ee18fed04b | ||
![]() |
28f56ba510 | ||
![]() |
c8d3dbb7b1 | ||
![]() |
a76a093638 | ||
![]() |
27908a8e17 | ||
![]() |
8a30f015c9 | ||
![]() |
8cac83fc96 | ||
![]() |
9ade4bb9b2 | ||
![]() |
874c91a086 | ||
![]() |
a906677440 | ||
![]() |
3f93942a24 | ||
![]() |
aeb3130b25 | ||
![]() |
8a6b364ca5 | ||
![]() |
2ade7328d1 | ||
![]() |
2bb9f4f444 | ||
![]() |
b029d983f9 | ||
![]() |
4082006039 | ||
![]() |
69aa0eaa7a | ||
![]() |
3674ada640 | ||
![]() |
48accb0a64 | ||
![]() |
70ac143cfe | ||
![]() |
b1b2d531f8 | ||
![]() |
e200783c59 | ||
![]() |
a7e57196c6 | ||
![]() |
b5f05e6cd2 | ||
![]() |
5fe5b35f21 | ||
![]() |
3610454a12 | ||
![]() |
abc4bbebe4 |
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -38,9 +38,9 @@ jobs:
|
||||
# Tests everything when JupyterHub works against a dedicated mysql or
|
||||
# postgresql server.
|
||||
#
|
||||
# jupyter_server:
|
||||
# nbclassic:
|
||||
# Tests everything when the user instances are started with
|
||||
# jupyter_server instead of notebook.
|
||||
# notebook instead of jupyter_server.
|
||||
#
|
||||
# ssl:
|
||||
# Tests everything using internal SSL connections instead of
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
#
|
||||
# main_dependencies:
|
||||
# Tests everything when the we use the latest available dependencies
|
||||
# from: ipytraitlets.
|
||||
# from: traitlets.
|
||||
#
|
||||
# NOTE: Since only the value of these parameters are presented in the
|
||||
# GitHub UI when the workflow run, we avoid using true/false as
|
||||
@@ -56,6 +56,7 @@ jobs:
|
||||
include:
|
||||
- python: "3.6"
|
||||
oldest_dependencies: oldest_dependencies
|
||||
nbclassic: nbclassic
|
||||
- python: "3.6"
|
||||
subdomain: subdomain
|
||||
- python: "3.7"
|
||||
@@ -65,7 +66,7 @@ jobs:
|
||||
- python: "3.8"
|
||||
db: postgres
|
||||
- python: "3.8"
|
||||
jupyter_server: jupyter_server
|
||||
nbclassic: nbclassic
|
||||
- python: "3.9"
|
||||
main_dependencies: main_dependencies
|
||||
|
||||
@@ -130,9 +131,9 @@ jobs:
|
||||
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
||||
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
||||
fi
|
||||
if [ "${{ matrix.jupyter_server }}" != "" ]; then
|
||||
pip uninstall notebook --yes
|
||||
pip install jupyter_server
|
||||
if [ "${{ matrix.nbclassic }}" != "" ]; then
|
||||
pip uninstall jupyter_server --yes
|
||||
pip install notebook
|
||||
fi
|
||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||
pip install mysql-connector-python
|
||||
@@ -194,7 +195,7 @@ jobs:
|
||||
|
||||
docker-build:
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.26.0
|
||||
rev: v2.29.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
@@ -10,15 +10,15 @@ repos:
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.8b0
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.4.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: "3.9.2"
|
||||
rev: "4.0.1"
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you’ve found a security vulnerability in a Jupyter
|
||||
project, please report it to security@ipython.org. If you prefer to
|
||||
encrypt your security reports, you can use [this PGP public key](https://jupyter-notebook.readthedocs.io/en/stable/_downloads/1d303a645f2505a8fd283826fafc9908/ipython_security.asc).
|
@@ -7,8 +7,8 @@ codecov
|
||||
coverage
|
||||
cryptography
|
||||
html5lib # needed for beautifulsoup
|
||||
jupyterlab >=3
|
||||
mock
|
||||
notebook
|
||||
pre-commit
|
||||
pytest>=3.3
|
||||
pytest-asyncio
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
Before installing JupyterHub, you will need:
|
||||
|
||||
- a Linux/Unix based system
|
||||
- [Python](https://www.python.org/downloads/) 3.5 or greater. An understanding
|
||||
of using [`pip`](https://pip.pypa.io/en/stable/) or
|
||||
- [Python](https://www.python.org/downloads/) 3.6 or greater. An understanding
|
||||
of using [`pip`](https://pip.pypa.io) or
|
||||
[`conda`](https://conda.io/docs/get-started.html) for
|
||||
installing Python packages is helpful.
|
||||
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
|
||||
@@ -20,11 +20,11 @@ Before installing JupyterHub, you will need:
|
||||
For example, install it on Linux (Debian/Ubuntu) using:
|
||||
|
||||
```
|
||||
sudo apt-get install npm nodejs-legacy
|
||||
sudo apt-get install nodejs npm
|
||||
```
|
||||
|
||||
The `nodejs-legacy` package installs the `node` executable and is currently
|
||||
required for npm to work on Debian/Ubuntu.
|
||||
[nodesource][] is a great resource to get more recent versions of the nodejs runtime,
|
||||
if your system package manager only has an old version of Node.js (e.g. 10 or older).
|
||||
|
||||
- A [pluggable authentication module (PAM)](https://en.wikipedia.org/wiki/Pluggable_authentication_module)
|
||||
to use the [default Authenticator](./getting-started/authenticators-users-basics.md).
|
||||
@@ -33,11 +33,17 @@ Before installing JupyterHub, you will need:
|
||||
- TLS certificate and key for HTTPS communication
|
||||
- Domain name
|
||||
|
||||
[nodesource]: https://github.com/nodesource/distributions#table-of-contents
|
||||
|
||||
Before running the single-user notebook servers (which may be on the same
|
||||
system as the Hub or not), you will need:
|
||||
|
||||
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
||||
version 4 or greater
|
||||
- [JupyterLab][] version 3 or greater,
|
||||
or [Jupyter Notebook][]
|
||||
4 or greater.
|
||||
|
||||
[jupyterlab]: https://jupyterlab.readthedocs.io
|
||||
[jupyter notebook]: https://jupyter.readthedocs.io/en/latest/install.html
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -48,14 +54,14 @@ JupyterHub can be installed with `pip` (and the proxy with `npm`) or `conda`:
|
||||
```bash
|
||||
python3 -m pip install jupyterhub
|
||||
npm install -g configurable-http-proxy
|
||||
python3 -m pip install notebook # needed if running the notebook servers locally
|
||||
python3 -m pip install jupyterlab notebook # needed if running the notebook servers in the same environment
|
||||
```
|
||||
|
||||
**conda** (one command installs jupyterhub and proxy):
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge jupyterhub # installs jupyterhub and proxy
|
||||
conda install notebook # needed if running the notebook servers locally
|
||||
conda install jupyterlab notebook # needed if running the notebook servers in the same environment
|
||||
```
|
||||
|
||||
Test your installation. If installed, these commands should return the packages'
|
||||
@@ -74,7 +80,7 @@ To start the Hub server, run the command:
|
||||
jupyterhub
|
||||
```
|
||||
|
||||
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
||||
Visit `http://localhost:8000` in your browser, and sign in with your unix
|
||||
credentials.
|
||||
|
||||
To **allow multiple users to sign in** to the Hub server, you must start
|
||||
|
@@ -76,13 +76,26 @@ c.InteractiveShellApp.extensions.append("cython")
|
||||
|
||||
### Example: Enable a Jupyter notebook configuration setting for all users
|
||||
|
||||
:::{note}
|
||||
These examples configure the Jupyter ServerApp,
|
||||
which is used by JupyterLab, the default in JupyterHub 2.0.
|
||||
|
||||
If you are using the classing Jupyter Notebook server,
|
||||
the same things should work,
|
||||
with the following substitutions:
|
||||
|
||||
- Where you see `jupyter_server_config`, use `jupyter_notebook_config`
|
||||
- Where you see `NotebookApp`, use `ServerApp`
|
||||
|
||||
:::
|
||||
|
||||
To enable Jupyter notebook's internal idle-shutdown behavior (requires
|
||||
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_notebook_config.py`
|
||||
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_server_config.py`
|
||||
file:
|
||||
|
||||
```python
|
||||
# shutdown the server after no activity for an hour
|
||||
c.NotebookApp.shutdown_no_activity_timeout = 60 * 60
|
||||
c.ServerApp.shutdown_no_activity_timeout = 60 * 60
|
||||
# shutdown kernels after no activity for 20 minutes
|
||||
c.MappingKernelManager.cull_idle_timeout = 20 * 60
|
||||
# check for idle kernels every two minutes
|
||||
@@ -112,8 +125,8 @@ Assuming I have a Python 2 and Python 3 environment that I want to make
|
||||
sure are available, I can install their specs system-wide (in /usr/local) with:
|
||||
|
||||
```bash
|
||||
/path/to/python3 -m IPython kernel install --prefix=/usr/local
|
||||
/path/to/python2 -m IPython kernel install --prefix=/usr/local
|
||||
/path/to/python3 -m ipykernel install --prefix=/usr/local
|
||||
/path/to/python2 -m ipykernel install --prefix=/usr/local
|
||||
```
|
||||
|
||||
## Multi-user hosts vs. Containers
|
||||
@@ -176,12 +189,40 @@ The number of named servers per user can be limited by setting
|
||||
c.JupyterHub.named_server_limit_per_user = 5
|
||||
```
|
||||
|
||||
## Switching to Jupyter Server
|
||||
(classic-notebook-ui)=
|
||||
|
||||
[Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/) is a new Tornado Server backend for Jupyter web applications (e.g. JupyterLab 3.0 uses this package as its default backend).
|
||||
## Switching back to classic notebook
|
||||
|
||||
By default, the single-user notebook server uses the (old) `NotebookApp` from the [notebook](https://github.com/jupyter/notebook) package. You can switch to using Jupyter Server's `ServerApp` backend (this will likely become the default in future releases) by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable to:
|
||||
By default the single-user server launches JupyterLab,
|
||||
which is based on [Jupyter Server][].
|
||||
This is the default server when running JupyterHub ≥ 2.0.
|
||||
You can switch to using the legacy Jupyter Notebook server by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
||||
(in the single-user environment) to:
|
||||
|
||||
```bash
|
||||
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||
```
|
||||
|
||||
[jupyter server]: https://jupyter-server.readthedocs.io
|
||||
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
||||
|
||||
:::{versionchanged} 2.0
|
||||
JupyterLab is now the default singleuser UI, if available,
|
||||
which is based on the [Jupyter Server][],
|
||||
no longer the legacy [Jupyter Notebook][] server.
|
||||
JupyterHub prior to 2.0 launched the legacy notebook server (`jupyter notebook`),
|
||||
and Jupyter server could be selected by specifying
|
||||
|
||||
```python
|
||||
# jupyterhub_config.py
|
||||
c.Spawner.cmd = ["jupyter-labhub"]
|
||||
```
|
||||
|
||||
or for an otherwise customized Jupyter Server app,
|
||||
set the environment variable:
|
||||
|
||||
```bash
|
||||
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
|
||||
```
|
||||
|
||||
:::
|
||||
|
@@ -29,7 +29,7 @@ def get_token():
|
||||
token_file = here.joinpath("service-token")
|
||||
log.info(f"Loading token from {token_file}")
|
||||
with token_file.open("r") as f:
|
||||
token = f.read()
|
||||
token = f.read().strip()
|
||||
return token
|
||||
|
||||
|
||||
|
@@ -5457,9 +5457,9 @@ npm-run-path@^4.0.0:
|
||||
path-key "^3.0.0"
|
||||
|
||||
nth-check@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
|
||||
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
|
||||
integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
@@ -7278,9 +7278,9 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tmpl@1.0.x:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
|
||||
integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
|
||||
|
||||
to-fast-properties@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@@ -6,7 +6,7 @@ version_info = (
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
"b1", # release (b1, rc1, or "" for final or dev)
|
||||
"b3", # release (b1, rc1, or "" for final or dev)
|
||||
# "dev", # dev or nothing for beta/rc/stable releases
|
||||
)
|
||||
|
||||
|
@@ -129,7 +129,7 @@ class GroupAPIHandler(_GroupAPIHandler):
|
||||
self.write(json.dumps(self.group_model(group)))
|
||||
self.set_status(201)
|
||||
|
||||
@needs_scope('admin:groups')
|
||||
@needs_scope('delete:groups')
|
||||
def delete(self, group_name):
|
||||
"""Delete a group by name"""
|
||||
group = self.find_group(group_name)
|
||||
|
@@ -266,7 +266,7 @@ class UserAPIHandler(APIHandler):
|
||||
self.write(json.dumps(self.user_model(user)))
|
||||
self.set_status(201)
|
||||
|
||||
@needs_scope('admin:users')
|
||||
@needs_scope('delete:users')
|
||||
async def delete(self, user_name):
|
||||
user = self.find_user(user_name)
|
||||
if user is None:
|
||||
@@ -421,6 +421,7 @@ class UserTokenListAPIHandler(APIHandler):
|
||||
token_model = self.token_model(orm.APIToken.find(self.db, api_token))
|
||||
token_model['token'] = api_token
|
||||
self.write(json.dumps(token_model))
|
||||
self.set_status(201)
|
||||
|
||||
|
||||
class UserTokenAPIHandler(APIHandler):
|
||||
@@ -525,7 +526,7 @@ class UserServerAPIHandler(APIHandler):
|
||||
self.set_header('Content-Type', 'text/plain')
|
||||
self.set_status(status)
|
||||
|
||||
@needs_scope('servers')
|
||||
@needs_scope('delete:servers')
|
||||
async def delete(self, user_name, server_name=''):
|
||||
user = self.find_user(user_name)
|
||||
options = self.get_json_body()
|
||||
|
@@ -1518,6 +1518,25 @@ class JupyterHub(Application):
|
||||
""",
|
||||
).tag(config=True)
|
||||
|
||||
use_legacy_stopped_server_status_code = Bool(
|
||||
False,
|
||||
help="""
|
||||
Return 503 rather than 424 when request comes in for a non-running server.
|
||||
|
||||
Prior to JupyterHub 2.0, we returned a 503 when any request came in for
|
||||
a user server that was currently not running. By default, JupyterHub 2.0
|
||||
will return a 424 - this makes operational metric dashboards more useful.
|
||||
|
||||
JupyterLab < 3.2 expected the 503 to know if the user server is no longer
|
||||
running, and prompted the user to start their server. Set this config to
|
||||
true to retain the old behavior, so JupyterLab < 3.2 can continue to show
|
||||
the appropriate UI when the user server is stopped.
|
||||
|
||||
This option will be removed in a future release.
|
||||
""",
|
||||
config=True,
|
||||
)
|
||||
|
||||
def init_handlers(self):
|
||||
h = []
|
||||
# load handlers from the authenticator
|
||||
|
@@ -1173,3 +1173,22 @@ class DummyAuthenticator(Authenticator):
|
||||
return data['username']
|
||||
return None
|
||||
return data['username']
|
||||
|
||||
|
||||
class NullAuthenticator(Authenticator):
|
||||
"""Null Authenticator for JupyterHub
|
||||
|
||||
For cases where authentication should be disabled,
|
||||
e.g. only allowing access via API tokens.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
# auto_login skips 'Login with...' page on Hub 0.8
|
||||
auto_login = True
|
||||
|
||||
# for Hub 0.7, show 'login with...'
|
||||
login_service = 'null'
|
||||
|
||||
def get_handlers(self, app):
|
||||
return []
|
||||
|
@@ -490,7 +490,7 @@ class BaseHandler(RequestHandler):
|
||||
session_id = self.get_session_cookie()
|
||||
if session_id:
|
||||
# clear session id
|
||||
self.clear_cookie(SESSION_COOKIE_NAME, **kwargs)
|
||||
self.clear_cookie(SESSION_COOKIE_NAME, path=self.base_url, **kwargs)
|
||||
|
||||
if user:
|
||||
# user is logged in, clear any tokens associated with the current session
|
||||
@@ -569,7 +569,9 @@ class BaseHandler(RequestHandler):
|
||||
so other services on this domain can read it.
|
||||
"""
|
||||
session_id = uuid.uuid4().hex
|
||||
self._set_cookie(SESSION_COOKIE_NAME, session_id, encrypted=False)
|
||||
self._set_cookie(
|
||||
SESSION_COOKIE_NAME, session_id, encrypted=False, path=self.base_url
|
||||
)
|
||||
return session_id
|
||||
|
||||
def set_service_cookie(self, user):
|
||||
@@ -1355,7 +1357,7 @@ class UserUrlHandler(BaseHandler):
|
||||
|
||||
**Changed Behavior as of 1.0** This handler no longer triggers a spawn. Instead, it checks if:
|
||||
|
||||
1. server is not active, serve page prompting for spawn (status: 503)
|
||||
1. server is not active, serve page prompting for spawn (status: 424)
|
||||
2. server is ready (This shouldn't happen! Proxy isn't updated yet. Wait a bit and redirect.)
|
||||
3. server is active, redirect to /hub/spawn-pending to monitor launch progress
|
||||
(will redirect back when finished)
|
||||
@@ -1374,7 +1376,14 @@ class UserUrlHandler(BaseHandler):
|
||||
self.log.warning(
|
||||
"Failing suspected API request to not-running server: %s", self.request.path
|
||||
)
|
||||
self.set_status(503)
|
||||
|
||||
# If we got here, the server is not running. To differentiate
|
||||
# that the *server* itself is not running, rather than just the particular
|
||||
# resource *in* the server is not found, we return a 424 instead of a 404.
|
||||
# We allow retaining the old behavior to support older JupyterLab versions
|
||||
self.set_status(
|
||||
424 if not self.app.use_legacy_stopped_server_status_code else 503
|
||||
)
|
||||
self.set_header("Content-Type", "application/json")
|
||||
|
||||
spawn_url = urlparse(self.request.full_url())._replace(query="")
|
||||
@@ -1539,15 +1548,17 @@ class UserUrlHandler(BaseHandler):
|
||||
self.redirect(pending_url, status=303)
|
||||
return
|
||||
|
||||
# if we got here, the server is not running
|
||||
# serve a page prompting for spawn and 503 error
|
||||
# visiting /user/:name no longer triggers implicit spawn
|
||||
# without explicit user action
|
||||
# If we got here, the server is not running. To differentiate
|
||||
# that the *server* itself is not running, rather than just the particular
|
||||
# page *in* the server is not found, we return a 424 instead of a 404.
|
||||
# We allow retaining the old behavior to support older JupyterLab versions
|
||||
spawn_url = url_concat(
|
||||
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
|
||||
{"next": self.request.uri},
|
||||
)
|
||||
self.set_status(503)
|
||||
self.set_status(
|
||||
424 if not self.app.use_legacy_stopped_server_status_code else 503
|
||||
)
|
||||
|
||||
auth_state = await user.get_auth_state()
|
||||
html = await self.render_template(
|
||||
|
@@ -89,6 +89,7 @@ def expand_self_scope(name):
|
||||
'users:activity',
|
||||
'read:users:activity',
|
||||
'servers',
|
||||
'delete:servers',
|
||||
'read:servers',
|
||||
'tokens',
|
||||
'read:tokens',
|
||||
|
@@ -36,13 +36,16 @@ scope_definitions = {
|
||||
},
|
||||
'admin:users': {
|
||||
'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.',
|
||||
'subscopes': ['admin:auth_state', 'users', 'read:roles:users'],
|
||||
'subscopes': ['admin:auth_state', 'users', 'read:roles:users', 'delete:users'],
|
||||
},
|
||||
'admin:auth_state': {'description': 'Read a user’s authentication state.'},
|
||||
'users': {
|
||||
'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).',
|
||||
'subscopes': ['read:users', 'list:users', 'users:activity'],
|
||||
},
|
||||
'delete:users': {
|
||||
'description': "Delete users.",
|
||||
},
|
||||
'list:users': {
|
||||
'description': 'List users, including at least their names.',
|
||||
'subscopes': ['read:users:name'],
|
||||
@@ -76,12 +79,13 @@ scope_definitions = {
|
||||
'admin:server_state': {'description': 'Read and write users’ server state.'},
|
||||
'servers': {
|
||||
'description': 'Start and stop user servers.',
|
||||
'subscopes': ['read:servers'],
|
||||
'subscopes': ['read:servers', 'delete:servers'],
|
||||
},
|
||||
'read:servers': {
|
||||
'description': 'Read users’ names and their server models (excluding the server state).',
|
||||
'subscopes': ['read:users:name'],
|
||||
},
|
||||
'delete:servers': {'description': "Stop and delete users' servers."},
|
||||
'tokens': {
|
||||
'description': 'Read, write, create and delete user tokens.',
|
||||
'subscopes': ['read:tokens'],
|
||||
@@ -89,7 +93,7 @@ scope_definitions = {
|
||||
'read:tokens': {'description': 'Read user tokens.'},
|
||||
'admin:groups': {
|
||||
'description': 'Read and write group information, create and delete groups.',
|
||||
'subscopes': ['groups', 'read:roles:groups'],
|
||||
'subscopes': ['groups', 'read:roles:groups', 'delete:groups'],
|
||||
},
|
||||
'groups': {
|
||||
'description': 'Read and write group information, including adding/removing users to/from groups.',
|
||||
@@ -104,6 +108,9 @@ scope_definitions = {
|
||||
'subscopes': ['read:groups:name'],
|
||||
},
|
||||
'read:groups:name': {'description': 'Read group names.'},
|
||||
'delete:groups': {
|
||||
'description': "Delete groups.",
|
||||
},
|
||||
'list:services': {
|
||||
'description': 'List services, including at least their names.',
|
||||
'subscopes': ['read:services:name'],
|
||||
|
@@ -1,7 +1,12 @@
|
||||
"""Make a single-user app based on the environment:
|
||||
|
||||
- $JUPYTERHUB_SINGLEUSER_APP, the base Application class, to be wrapped in JupyterHub authentication.
|
||||
default: notebook.notebookapp.NotebookApp
|
||||
default: jupyter_server.serverapp.ServerApp
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
Default app changed to launch `jupyter labhub`.
|
||||
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
|
||||
"""
|
||||
import os
|
||||
|
||||
@@ -9,12 +14,55 @@ from traitlets import import_item
|
||||
|
||||
from .mixins import make_singleuser_app
|
||||
|
||||
JUPYTERHUB_SINGLEUSER_APP = (
|
||||
os.environ.get("JUPYTERHUB_SINGLEUSER_APP") or "notebook.notebookapp.NotebookApp"
|
||||
)
|
||||
JUPYTERHUB_SINGLEUSER_APP = os.environ.get("JUPYTERHUB_SINGLEUSER_APP")
|
||||
|
||||
|
||||
if JUPYTERHUB_SINGLEUSER_APP:
|
||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||
else:
|
||||
App = None
|
||||
_import_error = None
|
||||
for JUPYTERHUB_SINGLEUSER_APP in (
|
||||
"jupyter_server.serverapp.ServerApp",
|
||||
"notebook.notebookapp.NotebookApp",
|
||||
):
|
||||
try:
|
||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||
except ImportError as e:
|
||||
continue
|
||||
if _import_error is None:
|
||||
_import_error = e
|
||||
else:
|
||||
break
|
||||
if App is None:
|
||||
raise _import_error
|
||||
|
||||
|
||||
SingleUserNotebookApp = make_singleuser_app(App)
|
||||
|
||||
main = SingleUserNotebookApp.launch_instance
|
||||
|
||||
def main():
|
||||
"""Launch a jupyterhub single-user server"""
|
||||
if not os.environ.get("JUPYTERHUB_SINGLEUSER_APP"):
|
||||
# app not specified, launch jupyter-labhub by default,
|
||||
# if jupyterlab is recent enough (3.1).
|
||||
# This is a minimally extended ServerApp that does:
|
||||
# 1. ensure lab extension is enabled, and
|
||||
# 2. set default URL to `/lab`
|
||||
import re
|
||||
|
||||
_version_pat = re.compile(r"(\d+)\.(\d+)")
|
||||
try:
|
||||
import jupyterlab
|
||||
from jupyterlab.labhubapp import SingleUserLabApp
|
||||
|
||||
m = _version_pat.match(jupyterlab.__version__)
|
||||
except Exception:
|
||||
m = None
|
||||
|
||||
if m is not None:
|
||||
version_tuple = tuple(int(v) for v in m.groups())
|
||||
if version_tuple >= (3, 1):
|
||||
return SingleUserLabApp.launch_instance()
|
||||
|
||||
return SingleUserNotebookApp.launch_instance()
|
||||
|
@@ -1366,8 +1366,8 @@ async def test_get_new_token_deprecated(app, headers, status):
|
||||
@mark.parametrize(
|
||||
"headers, status, note, expires_in",
|
||||
[
|
||||
({}, 200, 'test note', None),
|
||||
({}, 200, '', 100),
|
||||
({}, 201, 'test note', None),
|
||||
({}, 201, '', 100),
|
||||
({'Authorization': 'token bad'}, 403, '', None),
|
||||
],
|
||||
)
|
||||
@@ -1386,7 +1386,7 @@ async def test_get_new_token(app, headers, status, note, expires_in):
|
||||
app, 'users/admin/tokens', method='post', headers=headers, data=body
|
||||
)
|
||||
assert r.status_code == status
|
||||
if status != 200:
|
||||
if status != 201:
|
||||
return
|
||||
# check the new-token reply
|
||||
reply = r.json()
|
||||
@@ -1424,10 +1424,10 @@ async def test_get_new_token(app, headers, status, note, expires_in):
|
||||
@mark.parametrize(
|
||||
"as_user, for_user, status",
|
||||
[
|
||||
('admin', 'other', 200),
|
||||
('admin', 'other', 201),
|
||||
('admin', 'missing', 403),
|
||||
('user', 'other', 403),
|
||||
('user', 'user', 200),
|
||||
('user', 'user', 201),
|
||||
],
|
||||
)
|
||||
async def test_token_for_user(app, as_user, for_user, status):
|
||||
@@ -1448,7 +1448,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
||||
)
|
||||
assert r.status_code == status
|
||||
reply = r.json()
|
||||
if status != 200:
|
||||
if status != 201:
|
||||
return
|
||||
assert 'token' in reply
|
||||
|
||||
@@ -1486,7 +1486,7 @@ async def test_token_authenticator_noauth(app):
|
||||
data=json.dumps(data) if data else None,
|
||||
noauth=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 201
|
||||
reply = r.json()
|
||||
assert 'token' in reply
|
||||
r = await api_request(app, 'authorizations', 'token', reply['token'])
|
||||
@@ -1509,7 +1509,7 @@ async def test_token_authenticator_dict_noauth(app):
|
||||
data=json.dumps(data) if data else None,
|
||||
noauth=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 201
|
||||
reply = r.json()
|
||||
assert 'token' in reply
|
||||
r = await api_request(app, 'authorizations', 'token', reply['token'])
|
||||
|
@@ -3,6 +3,7 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import logging
|
||||
from unittest import mock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
from requests import HTTPError
|
||||
@@ -12,6 +13,8 @@ from .mocking import MockPAMAuthenticator
|
||||
from .mocking import MockStructGroup
|
||||
from .mocking import MockStructPasswd
|
||||
from .utils import add_user
|
||||
from .utils import async_requests
|
||||
from .utils import public_url
|
||||
from jupyterhub import auth
|
||||
from jupyterhub import crypto
|
||||
from jupyterhub import orm
|
||||
@@ -515,3 +518,12 @@ def test_deprecated_methods_subclass():
|
||||
assert authenticator.check_whitelist("subclass-allowed")
|
||||
assert not authenticator.check_allowed("otheruser")
|
||||
assert not authenticator.check_whitelist("otheruser")
|
||||
|
||||
|
||||
async def test_nullauthenticator(app):
|
||||
with mock.patch.dict(
|
||||
app.tornado_settings, {"authenticator": auth.NullAuthenticator(parent=app)}
|
||||
):
|
||||
r = await async_requests.get(public_url(app))
|
||||
assert urlparse(r.url).path.endswith("/hub/login")
|
||||
assert r.status_code == 403
|
||||
|
@@ -56,8 +56,8 @@ async def test_root_redirect(app):
|
||||
r = await get_page(url, app, cookies=cookies)
|
||||
path = urlparse(r.url).path
|
||||
assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name)
|
||||
# serve "server not running" page, which has status 503
|
||||
assert r.status_code == 503
|
||||
# serve "server not running" page, which has status 424
|
||||
assert r.status_code == 424
|
||||
|
||||
|
||||
async def test_root_default_url_noauth(app):
|
||||
@@ -172,7 +172,7 @@ async def test_spawn_redirect(app):
|
||||
r = await get_page('user/' + name, app, hub=False, cookies=cookies)
|
||||
path = urlparse(r.url).path
|
||||
assert path == ujoin(app.base_url, 'hub/user/%s/' % name)
|
||||
assert r.status_code == 503
|
||||
assert r.status_code == 424
|
||||
|
||||
|
||||
async def test_spawn_handler_access(app):
|
||||
@@ -507,13 +507,13 @@ async def test_user_redirect_deprecated(app, username):
|
||||
print(urlparse(r.url))
|
||||
path = urlparse(r.url).path
|
||||
assert path == ujoin(app.base_url, 'hub/user/%s/' % name)
|
||||
assert r.status_code == 503
|
||||
assert r.status_code == 424
|
||||
|
||||
r = await get_page('/user/baduser/test.ipynb', app, cookies=cookies, hub=False)
|
||||
print(urlparse(r.url))
|
||||
path = urlparse(r.url).path
|
||||
assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name)
|
||||
assert r.status_code == 503
|
||||
assert r.status_code == 424
|
||||
|
||||
r = await get_page('/user/baduser/test.ipynb', app, hub=False)
|
||||
r.raise_for_status()
|
||||
@@ -1061,13 +1061,20 @@ async def test_token_page(app):
|
||||
async def test_server_not_running_api_request(app):
|
||||
cookies = await app.login_user("bees")
|
||||
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
|
||||
assert r.status_code == 503
|
||||
assert r.status_code == 424
|
||||
assert r.headers["content-type"] == "application/json"
|
||||
message = r.json()['message']
|
||||
assert ujoin(app.base_url, "hub/spawn/bees") in message
|
||||
assert " /user/bees" in message
|
||||
|
||||
|
||||
async def test_server_not_running_api_request_legacy_status(app):
|
||||
app.use_legacy_stopped_server_status_code = True
|
||||
cookies = await app.login_user("bees")
|
||||
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
|
||||
assert r.status_code == 503
|
||||
|
||||
|
||||
async def test_metrics_no_auth(app):
|
||||
r = await get_page("metrics", app)
|
||||
assert r.status_code == 403
|
||||
|
@@ -182,6 +182,7 @@ def test_orm_roles_delete_cascade(db):
|
||||
'admin:users',
|
||||
'admin:auth_state',
|
||||
'users',
|
||||
'delete:users',
|
||||
'list:users',
|
||||
'read:users',
|
||||
'users:activity',
|
||||
@@ -218,6 +219,7 @@ def test_orm_roles_delete_cascade(db):
|
||||
{
|
||||
'admin:groups',
|
||||
'groups',
|
||||
'delete:groups',
|
||||
'list:groups',
|
||||
'read:groups',
|
||||
'read:roles:groups',
|
||||
@@ -229,6 +231,7 @@ def test_orm_roles_delete_cascade(db):
|
||||
{
|
||||
'admin:groups',
|
||||
'groups',
|
||||
'delete:groups',
|
||||
'list:groups',
|
||||
'read:groups',
|
||||
'read:roles:groups',
|
||||
@@ -658,11 +661,11 @@ async def test_load_roles_user_tokens(tmpdir, request):
|
||||
"headers, rolename, scopes, status",
|
||||
[
|
||||
# no role requested - gets default 'token' role
|
||||
({}, None, None, 200),
|
||||
({}, None, None, 201),
|
||||
# role scopes within the user's default 'user' role
|
||||
({}, 'self-reader', ['read:users'], 200),
|
||||
({}, 'self-reader', ['read:users'], 201),
|
||||
# role scopes outside of the user's role but within the group's role scopes of which the user is a member
|
||||
({}, 'groups-reader', ['read:groups'], 200),
|
||||
({}, 'groups-reader', ['read:groups'], 201),
|
||||
# non-existing role request
|
||||
({}, 'non-existing', [], 404),
|
||||
# role scopes outside of both user's role and group's role scopes
|
||||
|
@@ -1,6 +1,10 @@
|
||||
"""Tests for jupyterhub.singleuser"""
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from subprocess import check_output
|
||||
from unittest import mock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
@@ -14,6 +18,12 @@ from .utils import async_requests
|
||||
from .utils import AsyncSession
|
||||
|
||||
|
||||
@contextmanager
|
||||
def nullcontext():
|
||||
"""Python 3.7+ contextlib.nullcontext, backport for 3.6"""
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"access_scopes, server_name, expect_success",
|
||||
[
|
||||
@@ -171,3 +181,47 @@ def test_version():
|
||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--version']
|
||||
).decode('utf8', 'replace')
|
||||
assert jupyterhub.__version__ in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"JUPYTERHUB_SINGLEUSER_APP",
|
||||
[
|
||||
"",
|
||||
"notebook.notebookapp.NotebookApp",
|
||||
"jupyter_server.serverapp.ServerApp",
|
||||
],
|
||||
)
|
||||
def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
|
||||
try:
|
||||
import jupyter_server # noqa
|
||||
except ImportError:
|
||||
have_server = False
|
||||
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
|
||||
else:
|
||||
have_server = True
|
||||
expect_error = False
|
||||
|
||||
if expect_error:
|
||||
ctx = pytest.raises(CalledProcessError)
|
||||
else:
|
||||
ctx = nullcontext()
|
||||
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"JUPYTERHUB_SINGLEUSER_APP": JUPYTERHUB_SINGLEUSER_APP,
|
||||
},
|
||||
):
|
||||
with ctx:
|
||||
out = check_output(
|
||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
||||
).decode('utf8', 'replace')
|
||||
if expect_error:
|
||||
return
|
||||
# use help-all output to check inheritance
|
||||
if 'NotebookApp' in JUPYTERHUB_SINGLEUSER_APP or not have_server:
|
||||
assert '--NotebookApp.' in out
|
||||
assert '--ServerApp.' not in out
|
||||
else:
|
||||
assert '--ServerApp.' in out
|
||||
assert '--NotebookApp.' not in out
|
||||
|
@@ -84,7 +84,7 @@ class UserDict(dict):
|
||||
if user.name == key:
|
||||
key = user.id
|
||||
break
|
||||
return dict.__contains__(self, key)
|
||||
return super().__contains__(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""UserDict allows retrieval of user by any of:
|
||||
@@ -108,7 +108,7 @@ class UserDict(dict):
|
||||
if orm_user.id not in self:
|
||||
user = self[orm_user.id] = User(orm_user, self.settings)
|
||||
return user
|
||||
user = dict.__getitem__(self, orm_user.id)
|
||||
user = super().__getitem__(orm_user.id)
|
||||
user.db = self.db
|
||||
return user
|
||||
elif isinstance(key, int):
|
||||
@@ -119,7 +119,7 @@ class UserDict(dict):
|
||||
raise KeyError("No such user: %s" % id)
|
||||
user = self.add(orm_user)
|
||||
else:
|
||||
user = dict.__getitem__(self, id)
|
||||
user = super().__getitem__(id)
|
||||
return user
|
||||
else:
|
||||
raise KeyError(repr(key))
|
||||
@@ -145,7 +145,7 @@ class UserDict(dict):
|
||||
self.db.expunge(orm_spawner)
|
||||
if user.orm_user in self.db:
|
||||
self.db.expunge(user.orm_user)
|
||||
dict.__delitem__(self, user.id)
|
||||
super().__delitem__(user.id)
|
||||
|
||||
def delete(self, key):
|
||||
"""Delete a user from the cache and the database"""
|
||||
|
3
setup.py
3
setup.py
@@ -100,6 +100,7 @@ setup_args = dict(
|
||||
'default = jupyterhub.auth:PAMAuthenticator',
|
||||
'pam = jupyterhub.auth:PAMAuthenticator',
|
||||
'dummy = jupyterhub.auth:DummyAuthenticator',
|
||||
'null = jupyterhub.auth:NullAuthenticator',
|
||||
],
|
||||
'jupyterhub.proxies': [
|
||||
'default = jupyterhub.proxy:ConfigurableHTTPProxy',
|
||||
@@ -301,7 +302,7 @@ class develop_js_css(develop):
|
||||
if not self.uninstall:
|
||||
self.distribution.run_command('js')
|
||||
self.distribution.run_command('css')
|
||||
develop.run(self)
|
||||
super().run()
|
||||
|
||||
|
||||
setup_args['cmdclass']['develop'] = develop_js_css
|
||||
|
@@ -7,6 +7,6 @@ MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
||||
|
||||
ADD install_jupyterhub /tmp/install_jupyterhub
|
||||
ARG JUPYTERHUB_VERSION=main
|
||||
# install pinned jupyterhub and ensure notebook is installed
|
||||
# install pinned jupyterhub and ensure jupyterlab is installed
|
||||
RUN python3 /tmp/install_jupyterhub && \
|
||||
python3 -m pip install notebook
|
||||
python3 -m pip install jupyterlab
|
||||
|
Reference in New Issue
Block a user