mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-09 11:03:00 +00:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
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
|
# Tests everything when JupyterHub works against a dedicated mysql or
|
||||||
# postgresql server.
|
# postgresql server.
|
||||||
#
|
#
|
||||||
# jupyter_server:
|
# nbclassic:
|
||||||
# Tests everything when the user instances are started with
|
# Tests everything when the user instances are started with
|
||||||
# jupyter_server instead of notebook.
|
# notebook instead of jupyter_server.
|
||||||
#
|
#
|
||||||
# ssl:
|
# ssl:
|
||||||
# Tests everything using internal SSL connections instead of
|
# Tests everything using internal SSL connections instead of
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
#
|
#
|
||||||
# main_dependencies:
|
# main_dependencies:
|
||||||
# Tests everything when the we use the latest available 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
|
# NOTE: Since only the value of these parameters are presented in the
|
||||||
# GitHub UI when the workflow run, we avoid using true/false as
|
# GitHub UI when the workflow run, we avoid using true/false as
|
||||||
@@ -56,6 +56,7 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- python: "3.6"
|
- python: "3.6"
|
||||||
oldest_dependencies: oldest_dependencies
|
oldest_dependencies: oldest_dependencies
|
||||||
|
nbclassic: nbclassic
|
||||||
- python: "3.6"
|
- python: "3.6"
|
||||||
subdomain: subdomain
|
subdomain: subdomain
|
||||||
- python: "3.7"
|
- python: "3.7"
|
||||||
@@ -65,7 +66,7 @@ jobs:
|
|||||||
- python: "3.8"
|
- python: "3.8"
|
||||||
db: postgres
|
db: postgres
|
||||||
- python: "3.8"
|
- python: "3.8"
|
||||||
jupyter_server: jupyter_server
|
nbclassic: nbclassic
|
||||||
- python: "3.9"
|
- python: "3.9"
|
||||||
main_dependencies: main_dependencies
|
main_dependencies: main_dependencies
|
||||||
|
|
||||||
@@ -130,9 +131,9 @@ jobs:
|
|||||||
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
||||||
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.jupyter_server }}" != "" ]; then
|
if [ "${{ matrix.nbclassic }}" != "" ]; then
|
||||||
pip uninstall notebook --yes
|
pip uninstall jupyter_server --yes
|
||||||
pip install jupyter_server
|
pip install notebook
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||||
pip install mysql-connector-python
|
pip install mysql-connector-python
|
||||||
@@ -194,7 +195,7 @@ jobs:
|
|||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
timeout-minutes: 10
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.26.0
|
rev: v2.28.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
@@ -10,11 +10,11 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.8b0
|
rev: 21.9b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v2.4.0
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
@@ -7,8 +7,8 @@ codecov
|
|||||||
coverage
|
coverage
|
||||||
cryptography
|
cryptography
|
||||||
html5lib # needed for beautifulsoup
|
html5lib # needed for beautifulsoup
|
||||||
|
jupyterlab >=3
|
||||||
mock
|
mock
|
||||||
notebook
|
|
||||||
pre-commit
|
pre-commit
|
||||||
pytest>=3.3
|
pytest>=3.3
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
Before installing JupyterHub, you will need:
|
Before installing JupyterHub, you will need:
|
||||||
|
|
||||||
- a Linux/Unix based system
|
- a Linux/Unix based system
|
||||||
- [Python](https://www.python.org/downloads/) 3.5 or greater. An understanding
|
- [Python](https://www.python.org/downloads/) 3.6 or greater. An understanding
|
||||||
of using [`pip`](https://pip.pypa.io/en/stable/) or
|
of using [`pip`](https://pip.pypa.io) or
|
||||||
[`conda`](https://conda.io/docs/get-started.html) for
|
[`conda`](https://conda.io/docs/get-started.html) for
|
||||||
installing Python packages is helpful.
|
installing Python packages is helpful.
|
||||||
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
|
- [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:
|
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
|
[nodesource][] is a great resource to get more recent versions of the nodejs runtime,
|
||||||
required for npm to work on Debian/Ubuntu.
|
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)
|
- A [pluggable authentication module (PAM)](https://en.wikipedia.org/wiki/Pluggable_authentication_module)
|
||||||
to use the [default Authenticator](./getting-started/authenticators-users-basics.md).
|
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
|
- TLS certificate and key for HTTPS communication
|
||||||
- Domain name
|
- Domain name
|
||||||
|
|
||||||
|
[nodesource]: https://github.com/nodesource/distributions#table-of-contents
|
||||||
|
|
||||||
Before running the single-user notebook servers (which may be on the same
|
Before running the single-user notebook servers (which may be on the same
|
||||||
system as the Hub or not), you will need:
|
system as the Hub or not), you will need:
|
||||||
|
|
||||||
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
- [JupyterLab][] version 3 or greater,
|
||||||
version 4 or greater
|
or [Jupyter Notebook][]
|
||||||
|
4 or greater.
|
||||||
|
|
||||||
|
[jupyterlab]: https://jupyterlab.readthedocs.io
|
||||||
|
[jupyter notebook]: https://jupyter.readthedocs.io/en/latest/install.html
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -48,14 +54,14 @@ JupyterHub can be installed with `pip` (and the proxy with `npm`) or `conda`:
|
|||||||
```bash
|
```bash
|
||||||
python3 -m pip install jupyterhub
|
python3 -m pip install jupyterhub
|
||||||
npm install -g configurable-http-proxy
|
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):
|
**conda** (one command installs jupyterhub and proxy):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda install -c conda-forge jupyterhub # installs jupyterhub and proxy
|
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'
|
Test your installation. If installed, these commands should return the packages'
|
||||||
@@ -74,7 +80,7 @@ To start the Hub server, run the command:
|
|||||||
jupyterhub
|
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.
|
credentials.
|
||||||
|
|
||||||
To **allow multiple users to sign in** to the Hub server, you must start
|
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
|
### 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
|
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:
|
file:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# shutdown the server after no activity for an hour
|
# 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
|
# shutdown kernels after no activity for 20 minutes
|
||||||
c.MappingKernelManager.cull_idle_timeout = 20 * 60
|
c.MappingKernelManager.cull_idle_timeout = 20 * 60
|
||||||
# check for idle kernels every two minutes
|
# 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:
|
sure are available, I can install their specs system-wide (in /usr/local) with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/path/to/python3 -m IPython kernel install --prefix=/usr/local
|
/path/to/python3 -m ipykernel install --prefix=/usr/local
|
||||||
/path/to/python2 -m IPython kernel install --prefix=/usr/local
|
/path/to/python2 -m ipykernel install --prefix=/usr/local
|
||||||
```
|
```
|
||||||
|
|
||||||
## Multi-user hosts vs. Containers
|
## 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
|
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
|
```bash
|
||||||
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
|
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
@@ -29,7 +29,7 @@ def get_token():
|
|||||||
token_file = here.joinpath("service-token")
|
token_file = here.joinpath("service-token")
|
||||||
log.info(f"Loading token from {token_file}")
|
log.info(f"Loading token from {token_file}")
|
||||||
with token_file.open("r") as f:
|
with token_file.open("r") as f:
|
||||||
token = f.read()
|
token = f.read().strip()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
@@ -5457,9 +5457,9 @@ npm-run-path@^4.0.0:
|
|||||||
path-key "^3.0.0"
|
path-key "^3.0.0"
|
||||||
|
|
||||||
nth-check@^2.0.0:
|
nth-check@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
|
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
|
||||||
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
|
integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase "^1.0.0"
|
boolbase "^1.0.0"
|
||||||
|
|
||||||
@@ -7278,9 +7278,9 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
|||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
|
||||||
tmpl@1.0.x:
|
tmpl@1.0.x:
|
||||||
version "1.0.4"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
|
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||||
integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
|
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
|
||||||
|
|
||||||
to-fast-properties@^2.0.0:
|
to-fast-properties@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
@@ -6,7 +6,7 @@ version_info = (
|
|||||||
2,
|
2,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
"b1", # release (b1, rc1, or "" for final or dev)
|
"b2", # release (b1, rc1, or "" for final or dev)
|
||||||
# "dev", # dev or nothing for beta/rc/stable releases
|
# "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.write(json.dumps(self.group_model(group)))
|
||||||
self.set_status(201)
|
self.set_status(201)
|
||||||
|
|
||||||
@needs_scope('admin:groups')
|
@needs_scope('delete:groups')
|
||||||
def delete(self, group_name):
|
def delete(self, group_name):
|
||||||
"""Delete a group by name"""
|
"""Delete a group by name"""
|
||||||
group = self.find_group(group_name)
|
group = self.find_group(group_name)
|
||||||
|
@@ -266,7 +266,7 @@ class UserAPIHandler(APIHandler):
|
|||||||
self.write(json.dumps(self.user_model(user)))
|
self.write(json.dumps(self.user_model(user)))
|
||||||
self.set_status(201)
|
self.set_status(201)
|
||||||
|
|
||||||
@needs_scope('admin:users')
|
@needs_scope('delete:users')
|
||||||
async def delete(self, user_name):
|
async def delete(self, user_name):
|
||||||
user = self.find_user(user_name)
|
user = self.find_user(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -525,7 +525,7 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
self.set_header('Content-Type', 'text/plain')
|
self.set_header('Content-Type', 'text/plain')
|
||||||
self.set_status(status)
|
self.set_status(status)
|
||||||
|
|
||||||
@needs_scope('servers')
|
@needs_scope('delete:servers')
|
||||||
async def delete(self, user_name, server_name=''):
|
async def delete(self, user_name, server_name=''):
|
||||||
user = self.find_user(user_name)
|
user = self.find_user(user_name)
|
||||||
options = self.get_json_body()
|
options = self.get_json_body()
|
||||||
|
@@ -1173,3 +1173,22 @@ class DummyAuthenticator(Authenticator):
|
|||||||
return data['username']
|
return data['username']
|
||||||
return None
|
return None
|
||||||
return data['username']
|
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()
|
session_id = self.get_session_cookie()
|
||||||
if session_id:
|
if session_id:
|
||||||
# clear 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:
|
if user:
|
||||||
# user is logged in, clear any tokens associated with the current session
|
# 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.
|
so other services on this domain can read it.
|
||||||
"""
|
"""
|
||||||
session_id = uuid.uuid4().hex
|
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
|
return session_id
|
||||||
|
|
||||||
def set_service_cookie(self, user):
|
def set_service_cookie(self, user):
|
||||||
|
@@ -89,6 +89,7 @@ def expand_self_scope(name):
|
|||||||
'users:activity',
|
'users:activity',
|
||||||
'read:users:activity',
|
'read:users:activity',
|
||||||
'servers',
|
'servers',
|
||||||
|
'delete:servers',
|
||||||
'read:servers',
|
'read:servers',
|
||||||
'tokens',
|
'tokens',
|
||||||
'read:tokens',
|
'read:tokens',
|
||||||
|
@@ -36,13 +36,16 @@ scope_definitions = {
|
|||||||
},
|
},
|
||||||
'admin:users': {
|
'admin:users': {
|
||||||
'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.',
|
'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.'},
|
'admin:auth_state': {'description': 'Read a user’s authentication state.'},
|
||||||
'users': {
|
'users': {
|
||||||
'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).',
|
'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).',
|
||||||
'subscopes': ['read:users', 'list:users', 'users:activity'],
|
'subscopes': ['read:users', 'list:users', 'users:activity'],
|
||||||
},
|
},
|
||||||
|
'delete:users': {
|
||||||
|
'description': "Delete users.",
|
||||||
|
},
|
||||||
'list:users': {
|
'list:users': {
|
||||||
'description': 'List users, including at least their names.',
|
'description': 'List users, including at least their names.',
|
||||||
'subscopes': ['read:users:name'],
|
'subscopes': ['read:users:name'],
|
||||||
@@ -76,12 +79,13 @@ scope_definitions = {
|
|||||||
'admin:server_state': {'description': 'Read and write users’ server state.'},
|
'admin:server_state': {'description': 'Read and write users’ server state.'},
|
||||||
'servers': {
|
'servers': {
|
||||||
'description': 'Start and stop user servers.',
|
'description': 'Start and stop user servers.',
|
||||||
'subscopes': ['read:servers'],
|
'subscopes': ['read:servers', 'delete:servers'],
|
||||||
},
|
},
|
||||||
'read:servers': {
|
'read:servers': {
|
||||||
'description': 'Read users’ names and their server models (excluding the server state).',
|
'description': 'Read users’ names and their server models (excluding the server state).',
|
||||||
'subscopes': ['read:users:name'],
|
'subscopes': ['read:users:name'],
|
||||||
},
|
},
|
||||||
|
'delete:servers': {'description': "Stop and delete users' servers."},
|
||||||
'tokens': {
|
'tokens': {
|
||||||
'description': 'Read, write, create and delete user tokens.',
|
'description': 'Read, write, create and delete user tokens.',
|
||||||
'subscopes': ['read:tokens'],
|
'subscopes': ['read:tokens'],
|
||||||
@@ -89,7 +93,7 @@ scope_definitions = {
|
|||||||
'read:tokens': {'description': 'Read user tokens.'},
|
'read:tokens': {'description': 'Read user tokens.'},
|
||||||
'admin:groups': {
|
'admin:groups': {
|
||||||
'description': 'Read and write group information, create and delete groups.',
|
'description': 'Read and write group information, create and delete groups.',
|
||||||
'subscopes': ['groups', 'read:roles:groups'],
|
'subscopes': ['groups', 'read:roles:groups', 'delete:groups'],
|
||||||
},
|
},
|
||||||
'groups': {
|
'groups': {
|
||||||
'description': 'Read and write group information, including adding/removing users to/from groups.',
|
'description': 'Read and write group information, including adding/removing users to/from groups.',
|
||||||
@@ -104,6 +108,9 @@ scope_definitions = {
|
|||||||
'subscopes': ['read:groups:name'],
|
'subscopes': ['read:groups:name'],
|
||||||
},
|
},
|
||||||
'read:groups:name': {'description': 'Read group names.'},
|
'read:groups:name': {'description': 'Read group names.'},
|
||||||
|
'delete:groups': {
|
||||||
|
'description': "Delete groups.",
|
||||||
|
},
|
||||||
'list:services': {
|
'list:services': {
|
||||||
'description': 'List services, including at least their names.',
|
'description': 'List services, including at least their names.',
|
||||||
'subscopes': ['read:services:name'],
|
'subscopes': ['read:services:name'],
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
"""Make a single-user app based on the environment:
|
"""Make a single-user app based on the environment:
|
||||||
|
|
||||||
- $JUPYTERHUB_SINGLEUSER_APP, the base Application class, to be wrapped in JupyterHub authentication.
|
- $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
|
import os
|
||||||
|
|
||||||
@@ -9,12 +14,55 @@ from traitlets import import_item
|
|||||||
|
|
||||||
from .mixins import make_singleuser_app
|
from .mixins import make_singleuser_app
|
||||||
|
|
||||||
JUPYTERHUB_SINGLEUSER_APP = (
|
JUPYTERHUB_SINGLEUSER_APP = os.environ.get("JUPYTERHUB_SINGLEUSER_APP")
|
||||||
os.environ.get("JUPYTERHUB_SINGLEUSER_APP") or "notebook.notebookapp.NotebookApp"
|
|
||||||
)
|
|
||||||
|
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
|
||||||
|
|
||||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
|
||||||
|
|
||||||
SingleUserNotebookApp = make_singleuser_app(App)
|
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()
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
@@ -12,6 +13,8 @@ from .mocking import MockPAMAuthenticator
|
|||||||
from .mocking import MockStructGroup
|
from .mocking import MockStructGroup
|
||||||
from .mocking import MockStructPasswd
|
from .mocking import MockStructPasswd
|
||||||
from .utils import add_user
|
from .utils import add_user
|
||||||
|
from .utils import async_requests
|
||||||
|
from .utils import public_url
|
||||||
from jupyterhub import auth
|
from jupyterhub import auth
|
||||||
from jupyterhub import crypto
|
from jupyterhub import crypto
|
||||||
from jupyterhub import orm
|
from jupyterhub import orm
|
||||||
@@ -515,3 +518,12 @@ def test_deprecated_methods_subclass():
|
|||||||
assert authenticator.check_whitelist("subclass-allowed")
|
assert authenticator.check_whitelist("subclass-allowed")
|
||||||
assert not authenticator.check_allowed("otheruser")
|
assert not authenticator.check_allowed("otheruser")
|
||||||
assert not authenticator.check_whitelist("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
|
||||||
|
@@ -182,6 +182,7 @@ def test_orm_roles_delete_cascade(db):
|
|||||||
'admin:users',
|
'admin:users',
|
||||||
'admin:auth_state',
|
'admin:auth_state',
|
||||||
'users',
|
'users',
|
||||||
|
'delete:users',
|
||||||
'list:users',
|
'list:users',
|
||||||
'read:users',
|
'read:users',
|
||||||
'users:activity',
|
'users:activity',
|
||||||
@@ -218,6 +219,7 @@ def test_orm_roles_delete_cascade(db):
|
|||||||
{
|
{
|
||||||
'admin:groups',
|
'admin:groups',
|
||||||
'groups',
|
'groups',
|
||||||
|
'delete:groups',
|
||||||
'list:groups',
|
'list:groups',
|
||||||
'read:groups',
|
'read:groups',
|
||||||
'read:roles:groups',
|
'read:roles:groups',
|
||||||
@@ -229,6 +231,7 @@ def test_orm_roles_delete_cascade(db):
|
|||||||
{
|
{
|
||||||
'admin:groups',
|
'admin:groups',
|
||||||
'groups',
|
'groups',
|
||||||
|
'delete:groups',
|
||||||
'list:groups',
|
'list:groups',
|
||||||
'read:groups',
|
'read:groups',
|
||||||
'read:roles:groups',
|
'read:roles:groups',
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
"""Tests for jupyterhub.singleuser"""
|
"""Tests for jupyterhub.singleuser"""
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from subprocess import CalledProcessError
|
||||||
from subprocess import check_output
|
from subprocess import check_output
|
||||||
|
from unittest import mock
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,6 +18,12 @@ from .utils import async_requests
|
|||||||
from .utils import AsyncSession
|
from .utils import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def nullcontext():
|
||||||
|
"""Python 3.7+ contextlib.nullcontext, backport for 3.6"""
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"access_scopes, server_name, expect_success",
|
"access_scopes, server_name, expect_success",
|
||||||
[
|
[
|
||||||
@@ -171,3 +181,47 @@ def test_version():
|
|||||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--version']
|
[sys.executable, '-m', 'jupyterhub.singleuser', '--version']
|
||||||
).decode('utf8', 'replace')
|
).decode('utf8', 'replace')
|
||||||
assert jupyterhub.__version__ in out
|
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:
|
if user.name == key:
|
||||||
key = user.id
|
key = user.id
|
||||||
break
|
break
|
||||||
return dict.__contains__(self, key)
|
return super().__contains__(key)
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
"""UserDict allows retrieval of user by any of:
|
"""UserDict allows retrieval of user by any of:
|
||||||
@@ -108,7 +108,7 @@ class UserDict(dict):
|
|||||||
if orm_user.id not in self:
|
if orm_user.id not in self:
|
||||||
user = self[orm_user.id] = User(orm_user, self.settings)
|
user = self[orm_user.id] = User(orm_user, self.settings)
|
||||||
return user
|
return user
|
||||||
user = dict.__getitem__(self, orm_user.id)
|
user = super().__getitem__(orm_user.id)
|
||||||
user.db = self.db
|
user.db = self.db
|
||||||
return user
|
return user
|
||||||
elif isinstance(key, int):
|
elif isinstance(key, int):
|
||||||
@@ -119,7 +119,7 @@ class UserDict(dict):
|
|||||||
raise KeyError("No such user: %s" % id)
|
raise KeyError("No such user: %s" % id)
|
||||||
user = self.add(orm_user)
|
user = self.add(orm_user)
|
||||||
else:
|
else:
|
||||||
user = dict.__getitem__(self, id)
|
user = super().__getitem__(id)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
raise KeyError(repr(key))
|
raise KeyError(repr(key))
|
||||||
@@ -145,7 +145,7 @@ class UserDict(dict):
|
|||||||
self.db.expunge(orm_spawner)
|
self.db.expunge(orm_spawner)
|
||||||
if user.orm_user in self.db:
|
if user.orm_user in self.db:
|
||||||
self.db.expunge(user.orm_user)
|
self.db.expunge(user.orm_user)
|
||||||
dict.__delitem__(self, user.id)
|
super().__delitem__(user.id)
|
||||||
|
|
||||||
def delete(self, key):
|
def delete(self, key):
|
||||||
"""Delete a user from the cache and the database"""
|
"""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',
|
'default = jupyterhub.auth:PAMAuthenticator',
|
||||||
'pam = jupyterhub.auth:PAMAuthenticator',
|
'pam = jupyterhub.auth:PAMAuthenticator',
|
||||||
'dummy = jupyterhub.auth:DummyAuthenticator',
|
'dummy = jupyterhub.auth:DummyAuthenticator',
|
||||||
|
'null = jupyterhub.auth:NullAuthenticator',
|
||||||
],
|
],
|
||||||
'jupyterhub.proxies': [
|
'jupyterhub.proxies': [
|
||||||
'default = jupyterhub.proxy:ConfigurableHTTPProxy',
|
'default = jupyterhub.proxy:ConfigurableHTTPProxy',
|
||||||
@@ -301,7 +302,7 @@ class develop_js_css(develop):
|
|||||||
if not self.uninstall:
|
if not self.uninstall:
|
||||||
self.distribution.run_command('js')
|
self.distribution.run_command('js')
|
||||||
self.distribution.run_command('css')
|
self.distribution.run_command('css')
|
||||||
develop.run(self)
|
super().run()
|
||||||
|
|
||||||
|
|
||||||
setup_args['cmdclass']['develop'] = develop_js_css
|
setup_args['cmdclass']['develop'] = develop_js_css
|
||||||
|
@@ -7,6 +7,6 @@ MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
|||||||
|
|
||||||
ADD install_jupyterhub /tmp/install_jupyterhub
|
ADD install_jupyterhub /tmp/install_jupyterhub
|
||||||
ARG JUPYTERHUB_VERSION=main
|
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 && \
|
RUN python3 /tmp/install_jupyterhub && \
|
||||||
python3 -m pip install notebook
|
python3 -m pip install jupyterlab
|
||||||
|
Reference in New Issue
Block a user