mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
179 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bba81a856c | ||
![]() |
ca2a98695d | ||
![]() |
cbadb454d5 | ||
![]() |
0e26ba9f57 | ||
![]() |
261e2ae13e | ||
![]() |
ed6c981cf7 | ||
![]() |
a3e642150e | ||
![]() |
53fb794241 | ||
![]() |
0e27bac90e | ||
![]() |
a15656c1cf | ||
![]() |
f2da9774b3 | ||
![]() |
1978c36985 | ||
![]() |
da835fbe86 | ||
![]() |
faa34044f3 | ||
![]() |
e196c93783 | ||
![]() |
26d5ee3eba | ||
![]() |
dc69ff4126 | ||
![]() |
024fe661e5 | ||
![]() |
11bc5c325a | ||
![]() |
642475f844 | ||
![]() |
6f05534dd8 | ||
![]() |
750df8b686 | ||
![]() |
485ac0df4c | ||
![]() |
b180bd0c0c | ||
![]() |
0a6f0165b7 | ||
![]() |
21b77e2348 | ||
![]() |
b5e43b7dfb | ||
![]() |
eab7e54d3d | ||
![]() |
2b3a9d9ab8 | ||
![]() |
fb4872f74d | ||
![]() |
efbd593113 | ||
![]() |
e390ba0e4d | ||
![]() |
9c2ca005b5 | ||
![]() |
3861163bbb | ||
![]() |
b105fe14dc | ||
![]() |
950d98ee57 | ||
![]() |
6d2f47150b | ||
![]() |
203cbe291e | ||
![]() |
f22c239666 | ||
![]() |
783ddf5265 | ||
![]() |
c92ef8bd45 | ||
![]() |
ae06035711 | ||
![]() |
074917d9be | ||
![]() |
8ae8f75516 | ||
![]() |
e8aef6587e | ||
![]() |
6f776053e8 | ||
![]() |
42ee0f9797 | ||
![]() |
7090444ce4 | ||
![]() |
818964fd3a | ||
![]() |
63383ce9db | ||
![]() |
89d9e43d3c | ||
![]() |
60944e48bf | ||
![]() |
a24292b54c | ||
![]() |
a14792decd | ||
![]() |
ad358a9884 | ||
![]() |
0e4c6c6581 | ||
![]() |
cbace1de16 | ||
![]() |
af7ccfc117 | ||
![]() |
1ef87fb41a | ||
![]() |
093dea9bcf | ||
![]() |
9e0c75884c | ||
![]() |
16e5080ae9 | ||
![]() |
5abf4bdb75 | ||
![]() |
225e87d9db | ||
![]() |
d923b9b736 | ||
![]() |
0b98bcd503 | ||
![]() |
d38e41fd97 | ||
![]() |
37fd7af917 | ||
![]() |
20895dba83 | ||
![]() |
2079d1e7c4 | ||
![]() |
1b0355b173 | ||
![]() |
df11d83d2c | ||
![]() |
83db40b01f | ||
![]() |
12dc3a9ff8 | ||
![]() |
61c48fd453 | ||
![]() |
45294dfdc7 | ||
![]() |
46ccb3cd4a | ||
![]() |
0a0b20834f | ||
![]() |
3a26e66adc | ||
![]() |
94faddb1e0 | ||
![]() |
d1ebd8e5bf | ||
![]() |
a59b33686a | ||
![]() |
b6d0a62c75 | ||
![]() |
0404ba6433 | ||
![]() |
f707ff372d | ||
![]() |
370c649d61 | ||
![]() |
1fe10713fd | ||
![]() |
ed80a8232f | ||
![]() |
0884ebc948 | ||
![]() |
1da7eee9ba | ||
![]() |
d92226134d | ||
![]() |
d9f2ec0b8e | ||
![]() |
90c95d5665 | ||
![]() |
1b00e49e4d | ||
![]() |
3e09b979bc | ||
![]() |
e81884fabb | ||
![]() |
3f7334e960 | ||
![]() |
548744e59b | ||
![]() |
015370eec7 | ||
![]() |
c0fd37cbeb | ||
![]() |
74b5e2601d | ||
![]() |
a28fb9361f | ||
![]() |
98d13d8e74 | ||
![]() |
98ef84e774 | ||
![]() |
cd2a311f54 | ||
![]() |
cd373049ed | ||
![]() |
8b7b7ad67e | ||
![]() |
76bd0a4aa2 | ||
![]() |
2e66cabe8d | ||
![]() |
7819b5cc3e | ||
![]() |
6411c25c28 | ||
![]() |
de7ee551d7 | ||
![]() |
a035d7f65e | ||
![]() |
cb998f0c0d | ||
![]() |
9cbe5eae5b | ||
![]() |
73313fdef8 | ||
![]() |
4d5828fa8c | ||
![]() |
0b6500fe21 | ||
![]() |
7ad9fee198 | ||
![]() |
09ead8cacc | ||
![]() |
3be375e12c | ||
![]() |
6f71a3a5a2 | ||
![]() |
f336c77166 | ||
![]() |
7c1ca033f3 | ||
![]() |
71f085fc19 | ||
![]() |
e7388b4333 | ||
![]() |
5c4100a4d0 | ||
![]() |
0e643ae274 | ||
![]() |
8423d81cf3 | ||
![]() |
2942654f15 | ||
![]() |
55aa910177 | ||
![]() |
b809311582 | ||
![]() |
73b2c408e1 | ||
![]() |
5265ff4165 | ||
![]() |
954ce155e0 | ||
![]() |
1a750c0479 | ||
![]() |
43323d0f60 | ||
![]() |
65a87bcf65 | ||
![]() |
0e44693819 | ||
![]() |
faa5e31f52 | ||
![]() |
01a43f41f8 | ||
![]() |
d008d51b7f | ||
![]() |
68b2dbf0f5 | ||
![]() |
f07b55c289 | ||
![]() |
e6ab7ae58d | ||
![]() |
fbc752e352 | ||
![]() |
6b5d87da63 | ||
![]() |
cd30074ab9 | ||
![]() |
4d4ded311e | ||
![]() |
9f5f02aa73 | ||
![]() |
2babc7ae83 | ||
![]() |
27c1441baa | ||
![]() |
bc21e99e7e | ||
![]() |
07b6e281c4 | ||
![]() |
5023718463 | ||
![]() |
4617cc10ef | ||
![]() |
b35d77f475 | ||
![]() |
68f784edee | ||
![]() |
e3b704b83e | ||
![]() |
a8a26856b0 | ||
![]() |
f087178171 | ||
![]() |
3c5dd08c17 | ||
![]() |
072dc29f80 | ||
![]() |
c9500e5b71 | ||
![]() |
caae054cea | ||
![]() |
2c01935339 | ||
![]() |
0ef744e4c9 | ||
![]() |
e405a71a19 | ||
![]() |
798faaafe8 | ||
![]() |
b673fad94b | ||
![]() |
f45f7536e9 | ||
![]() |
e06abc3158 | ||
![]() |
e4514725cf | ||
![]() |
af655f9be1 | ||
![]() |
76488db2ef | ||
![]() |
36fd86798e | ||
![]() |
46efc3e689 | ||
![]() |
71bbdf65ac | ||
![]() |
1bcc508e42 |
7
.github/dependabot.yaml
vendored
7
.github/dependabot.yaml
vendored
@@ -53,5 +53,12 @@ updates:
|
||||
- "*-loader"
|
||||
update-types:
|
||||
- major
|
||||
# group major bumps of jest-related dependencies
|
||||
jsx-jest:
|
||||
patterns:
|
||||
- "*jest*"
|
||||
- "*test*"
|
||||
update-types:
|
||||
- major
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -35,13 +35,13 @@ jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
|
8
.github/workflows/test-docs.yml
vendored
8
.github/workflows/test-docs.yml
vendored
@@ -41,9 +41,9 @@ jobs:
|
||||
validate-rest-api-definition:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
@@ -55,13 +55,13 @@ jobs:
|
||||
test-docs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
# make rediraffecheckdiff requires git history to compare current
|
||||
# commit with the main branch and previous releases.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
|
4
.github/workflows/test-jsx.yml
vendored
4
.github/workflows/test-jsx.yml
vendored
@@ -32,8 +32,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -139,11 +139,11 @@ jobs:
|
||||
if [ "${{ matrix.jupyverse }}" != "" ]; then
|
||||
echo "JUPYTERHUB_SINGLEUSER_APP=jupyverse" >> $GITHUB_ENV
|
||||
fi
|
||||
- uses: actions/checkout@v4
|
||||
# NOTE: actions/setup-node@v4 make use of a cache within the GitHub base
|
||||
- uses: actions/checkout@v5
|
||||
# NOTE: actions/setup-node@v5 make use of a cache within the GitHub base
|
||||
# environment and setup in a fraction of a second.
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Install Javascript dependencies
|
||||
@@ -152,10 +152,10 @@ jobs:
|
||||
npm install -g configurable-http-proxy yarn
|
||||
npm list
|
||||
|
||||
# NOTE: actions/setup-python@v5 make use of a cache within the GitHub base
|
||||
# NOTE: actions/setup-python@v6 make use of a cache within the GitHub base
|
||||
# environment and setup in a fraction of a second.
|
||||
- name: Install Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "${{ matrix.python }}"
|
||||
cache: pip
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
# make sure our `>=` pins really do express our minimum supported versions
|
||||
pip install -r ci/oldest-dependencies/requirements.old -e .
|
||||
else
|
||||
pip install --pre -e ".[test]"
|
||||
pip install --pre -e ".[test]" "pycurl; python_version >= '3.10'"
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
||||
|
@@ -16,7 +16,7 @@ ci:
|
||||
repos:
|
||||
# autoformat and lint Python code
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.4
|
||||
rev: v0.12.11
|
||||
hooks:
|
||||
- id: ruff
|
||||
types_or:
|
||||
@@ -29,8 +29,8 @@ repos:
|
||||
- jupyter
|
||||
|
||||
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
exclude: .*/templates/.*|docs/source/_static/rest-api.yml|docs/source/rbac/scope-table.md
|
||||
@@ -49,7 +49,7 @@ repos:
|
||||
|
||||
# Autoformat and linting, misc. details
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: share/jupyterhub/static/js/admin-react.js
|
||||
|
@@ -37,4 +37,4 @@ The Jupyter Development Team is the set of all contributors to the Jupyter proje
|
||||
This includes all of the Jupyter subprojects.
|
||||
|
||||
The team that coordinates JupyterHub subproject can be found here:
|
||||
https://jupyterhub-team-compass.readthedocs.io/en/latest/governance.html
|
||||
https://compass.hub.jupyter.org/page/governance.html
|
||||
|
@@ -58,7 +58,6 @@ for administration of the Hub and its users.
|
||||
- A Linux/Unix based system
|
||||
- [Python](https://www.python.org/downloads/) 3.8 or greater
|
||||
- [nodejs/npm](https://www.npmjs.com/)
|
||||
|
||||
- If you are using **`conda`**, the nodejs and npm dependencies will be installed for
|
||||
you by conda.
|
||||
|
||||
|
@@ -1,5 +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).
|
||||
project, please report it!
|
||||
See the [security documentation](https://jupyterhub.readthedocs.org/en/latest/contributing/security.html) for how.
|
||||
|
@@ -7,7 +7,7 @@ info:
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
identifier: BSD-3-Clause
|
||||
version: 5.3.0
|
||||
version: 5.4.0
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
@@ -62,18 +62,19 @@ paths:
|
||||
properties:
|
||||
class:
|
||||
type: string
|
||||
description: The Python class currently active for JupyterHub
|
||||
Authentication
|
||||
description: The Python class currently active for
|
||||
JupyterHub Authentication
|
||||
version:
|
||||
type: string
|
||||
description: The version of the currently active Authenticator
|
||||
description: The version of the currently active
|
||||
Authenticator
|
||||
spawner:
|
||||
type: object
|
||||
properties:
|
||||
class:
|
||||
type: string
|
||||
description: The Python class currently active for spawning
|
||||
single-user notebook servers
|
||||
description: The Python class currently active for
|
||||
spawning single-user notebook servers
|
||||
version:
|
||||
type: string
|
||||
description: The version of the currently active Spawner
|
||||
@@ -256,8 +257,8 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/userName"
|
||||
requestBody:
|
||||
description: Updated user info. At least one key to be updated (name or admin)
|
||||
is required.
|
||||
description: Updated user info. At least one key to be updated (name or
|
||||
admin) is required.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -265,12 +266,12 @@ paths:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: the new name (optional, if another key is updated i.e.
|
||||
admin)
|
||||
description: the new name (optional, if another key is updated
|
||||
i.e. admin)
|
||||
admin:
|
||||
type: boolean
|
||||
description: update admin (optional, if another key is updated i.e.
|
||||
name)
|
||||
description: update admin (optional, if another key is updated
|
||||
i.e. name)
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
@@ -286,8 +287,8 @@ paths:
|
||||
post:
|
||||
operationId: post-user-activity
|
||||
summary: Notify Hub of activity for a given user
|
||||
description: Notify the Hub of activity by the user, e.g. accessing a service
|
||||
or (more likely) actively using a server.
|
||||
description: Notify the Hub of activity by the user, e.g. accessing a
|
||||
service or (more likely) actively using a server.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/userName"
|
||||
requestBody:
|
||||
@@ -366,8 +367,8 @@ paths:
|
||||
description: The user's notebook server has started
|
||||
content: {}
|
||||
202:
|
||||
description: The user's notebook server has not yet started, but has been
|
||||
requested
|
||||
description: The user's notebook server has not yet started, but has
|
||||
been requested
|
||||
content: {}
|
||||
security:
|
||||
- oauth2:
|
||||
@@ -380,8 +381,8 @@ paths:
|
||||
- $ref: "#/components/parameters/userName"
|
||||
responses:
|
||||
202:
|
||||
description: The user's notebook server has not yet stopped as it is taking
|
||||
a while to stop
|
||||
description: The user's notebook server has not yet stopped as it is
|
||||
taking a while to stop
|
||||
content: {}
|
||||
204:
|
||||
description: The user's notebook server has stopped
|
||||
@@ -412,8 +413,8 @@ paths:
|
||||
description: The user's notebook named-server has started
|
||||
content: {}
|
||||
202:
|
||||
description: The user's notebook named-server has not yet started, but has
|
||||
been requested
|
||||
description: The user's notebook named-server has not yet started, but
|
||||
has been requested
|
||||
content: {}
|
||||
security:
|
||||
- oauth2:
|
||||
@@ -448,8 +449,8 @@ paths:
|
||||
required: false
|
||||
responses:
|
||||
202:
|
||||
description: The user's notebook named-server has not yet stopped as it
|
||||
is taking a while to stop
|
||||
description: The user's notebook named-server has not yet stopped as
|
||||
it is taking a while to stop
|
||||
content: {}
|
||||
204:
|
||||
description: The user's notebook named-server has stopped
|
||||
@@ -462,8 +463,8 @@ paths:
|
||||
get:
|
||||
operationId: get-user-shared
|
||||
summary: List servers shared with user
|
||||
description: Returns list of Shares granting the user access to servers owned
|
||||
by others (new in 5.0)
|
||||
description: Returns list of Shares granting the user access to servers
|
||||
owned by others (new in 5.0)
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/userName"
|
||||
|
||||
@@ -576,11 +577,13 @@ paths:
|
||||
expires_in:
|
||||
type: number
|
||||
example: 3600
|
||||
description: lifetime (in seconds) after which the requested token
|
||||
will expire. Omit, or specify null or 0 for no expiration.
|
||||
description: lifetime (in seconds) after which the requested
|
||||
token will expire. Omit, or specify null or 0 for no
|
||||
expiration.
|
||||
note:
|
||||
type: string
|
||||
description: A note attached to the token for future bookkeeping
|
||||
description: A note attached to the token for future
|
||||
bookkeeping
|
||||
roles:
|
||||
type: array
|
||||
description: |
|
||||
@@ -758,7 +761,8 @@ paths:
|
||||
- $ref: "#/components/parameters/sharedServerName"
|
||||
responses:
|
||||
200:
|
||||
description: The permissions granted to members of `group` on `owner/server`
|
||||
description: The permissions granted to members of `group` on
|
||||
`owner/server`
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -1173,7 +1177,8 @@ paths:
|
||||
description: |
|
||||
The full URL for accepting the code,
|
||||
if JupyterHub.public_url configuration is defined.
|
||||
example: https://hub.example.org/hub/accept-share?code=abc123
|
||||
example:
|
||||
https://hub.example.org/hub/accept-share?code=abc123
|
||||
security:
|
||||
- oauth2:
|
||||
- shares
|
||||
@@ -1250,8 +1255,8 @@ paths:
|
||||
get:
|
||||
operationId: get-proxy
|
||||
summary: Get the proxy's routing table
|
||||
description: A convenience alias for getting the routing table directly from
|
||||
the proxy
|
||||
description: A convenience alias for getting the routing table directly
|
||||
from the proxy
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/paginationOffset"
|
||||
- $ref: "#/components/parameters/paginationLimit"
|
||||
@@ -1262,8 +1267,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
description: configurable-http-proxy routing table (see configurable-http-proxy
|
||||
docs for details)
|
||||
description: configurable-http-proxy routing table (see
|
||||
configurable-http-proxy docs for details)
|
||||
security:
|
||||
- oauth2:
|
||||
- proxy
|
||||
@@ -1282,8 +1287,8 @@ paths:
|
||||
summary: Notify the Hub about a new proxy
|
||||
description: Notifies the Hub of a new proxy to use.
|
||||
requestBody:
|
||||
description: Any values that have changed for the new proxy. All keys are
|
||||
optional.
|
||||
description: Any values that have changed for the new proxy. All keys
|
||||
are optional.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -1374,8 +1379,8 @@ paths:
|
||||
get:
|
||||
operationId: get-auth-cookie
|
||||
summary: Identify a user from a cookie
|
||||
description: Used by single-user notebook servers to hand off cookie authentication
|
||||
to the Hub
|
||||
description: Used by single-user notebook servers to hand off cookie
|
||||
authentication to the Hub
|
||||
parameters:
|
||||
- name: cookie_name
|
||||
in: path
|
||||
@@ -1499,12 +1504,12 @@ paths:
|
||||
properties:
|
||||
proxy:
|
||||
type: boolean
|
||||
description: Whether the proxy should be shutdown as well (default
|
||||
from Hub config)
|
||||
description: Whether the proxy should be shutdown as well
|
||||
(default from Hub config)
|
||||
servers:
|
||||
type: boolean
|
||||
description: Whether users' notebook servers should be shutdown
|
||||
as well (default from Hub config)
|
||||
description: Whether users' notebook servers should be
|
||||
shutdown as well (default from Hub config)
|
||||
required: false
|
||||
responses:
|
||||
202:
|
||||
@@ -1648,8 +1653,8 @@ components:
|
||||
type: string
|
||||
server:
|
||||
type: string
|
||||
description: The user's notebook server's base URL, if running; null if
|
||||
not.
|
||||
description: The user's notebook server's base URL, if running; null
|
||||
if not.
|
||||
pending:
|
||||
type: string
|
||||
description: The currently pending action, if any
|
||||
@@ -1680,8 +1685,8 @@ components:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The server's name. The user's default server has an empty name
|
||||
('')
|
||||
description: The server's name. The user's default server has an empty
|
||||
name ('')
|
||||
ready:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -1743,14 +1748,14 @@ components:
|
||||
state:
|
||||
type: object
|
||||
properties: {}
|
||||
description: Arbitrary internal state from this server's spawner. Only available
|
||||
on the hub's users list or get-user-by-name method, and only with admin:users:server_state
|
||||
scope. None otherwise.
|
||||
description: Arbitrary internal state from this server's spawner. Only
|
||||
available on the hub's users list or get-user-by-name method, and
|
||||
only with admin:users:server_state scope. None otherwise.
|
||||
user_options:
|
||||
type: object
|
||||
properties: {}
|
||||
description: User specified options for the user's spawned instance of a
|
||||
single-user server.
|
||||
description: User specified options for the user's spawned instance of
|
||||
a single-user server.
|
||||
RequestIdentity:
|
||||
description: |
|
||||
The model for the entity making the request.
|
||||
@@ -1918,8 +1923,8 @@ components:
|
||||
items:
|
||||
type: string
|
||||
group:
|
||||
description: the group being shared with (exactly one of 'user' or 'group'
|
||||
will be non-null, the other will be null)
|
||||
description: the group being shared with (exactly one of 'user' or
|
||||
'group' will be non-null, the other will be null)
|
||||
type:
|
||||
- object
|
||||
- "null"
|
||||
@@ -1927,8 +1932,8 @@ components:
|
||||
name:
|
||||
type: string
|
||||
user:
|
||||
description: the user being shared with (exactly one of 'user' or 'group'
|
||||
will be non-null, the other will be null)
|
||||
description: the user being shared with (exactly one of 'user' or
|
||||
'group' will be non-null, the other will be null)
|
||||
type:
|
||||
- object
|
||||
- "null"
|
||||
@@ -1941,8 +1946,8 @@ components:
|
||||
format: date-time
|
||||
|
||||
ShareCode:
|
||||
description: A single sharing code. There is at most one of these objects per
|
||||
(server, user) or (server, group) combination.
|
||||
description: A single sharing code. There is at most one of these objects
|
||||
per (server, user) or (server, group) combination.
|
||||
type: object
|
||||
properties:
|
||||
server:
|
||||
@@ -1977,37 +1982,41 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The id of the API token. Used for modifying or deleting the
|
||||
token.
|
||||
description: The id of the API token. Used for modifying or deleting
|
||||
the token.
|
||||
user:
|
||||
type: string
|
||||
description: The user that owns a token (undefined if owned by a service)
|
||||
description: The user that owns a token (undefined if owned by a
|
||||
service)
|
||||
service:
|
||||
type: string
|
||||
description: The service that owns the token (undefined of owned by a user)
|
||||
description: The service that owns the token (undefined of owned by a
|
||||
user)
|
||||
roles:
|
||||
type: array
|
||||
description: Deprecated in JupyterHub 3, always an empty list. Tokens have
|
||||
'scopes' starting from JupyterHub 3.
|
||||
description: Deprecated in JupyterHub 3, always an empty list. Tokens
|
||||
have 'scopes' starting from JupyterHub 3.
|
||||
items:
|
||||
type: string
|
||||
scopes:
|
||||
type: array
|
||||
description: List of scopes this token has been assigned. New in JupyterHub
|
||||
3. In JupyterHub 2.x, tokens were assigned 'roles' instead of scopes.
|
||||
description: List of scopes this token has been assigned. New in
|
||||
JupyterHub 3. In JupyterHub 2.x, tokens were assigned 'roles'
|
||||
instead of scopes.
|
||||
items:
|
||||
type: string
|
||||
note:
|
||||
type: string
|
||||
description: A note about the token, typically describing what it was created
|
||||
for.
|
||||
description: A note about the token, typically describing what it was
|
||||
created for.
|
||||
created:
|
||||
type: string
|
||||
description: Timestamp when this token was created
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: string
|
||||
description: Timestamp when this token expires. Null if there is no expiry.
|
||||
description: Timestamp when this token expires. Null if there is no
|
||||
expiry.
|
||||
format: date-time
|
||||
last_activity:
|
||||
type: string
|
||||
@@ -2030,41 +2039,45 @@ components:
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The token itself. Only present in responses to requests for
|
||||
a new token.
|
||||
description: The token itself. Only present in responses to requests
|
||||
for a new token.
|
||||
id:
|
||||
type: string
|
||||
description: The id of the API token. Used for modifying or deleting the
|
||||
token.
|
||||
description: The id of the API token. Used for modifying or deleting
|
||||
the token.
|
||||
user:
|
||||
type: string
|
||||
description: The user that owns a token (undefined if owned by a service)
|
||||
description: The user that owns a token (undefined if owned by a
|
||||
service)
|
||||
service:
|
||||
type: string
|
||||
description: The service that owns the token (undefined of owned by a user)
|
||||
description: The service that owns the token (undefined of owned by a
|
||||
user)
|
||||
roles:
|
||||
type: array
|
||||
description: Deprecated in JupyterHub 3, always an empty list. Tokens have
|
||||
'scopes' starting from JupyterHub 3.
|
||||
description: Deprecated in JupyterHub 3, always an empty list. Tokens
|
||||
have 'scopes' starting from JupyterHub 3.
|
||||
items:
|
||||
type: string
|
||||
scopes:
|
||||
type: array
|
||||
description: List of scopes this token has been assigned. New in JupyterHub
|
||||
3. In JupyterHub 2.x, tokens were assigned 'roles' instead of scopes.
|
||||
description: List of scopes this token has been assigned. New in
|
||||
JupyterHub 3. In JupyterHub 2.x, tokens were assigned 'roles'
|
||||
instead of scopes.
|
||||
items:
|
||||
type: string
|
||||
note:
|
||||
type: string
|
||||
description: A note about the token, typically describing what it was created
|
||||
for.
|
||||
description: A note about the token, typically describing what it was
|
||||
created for.
|
||||
created:
|
||||
type: string
|
||||
description: Timestamp when this token was created
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: string
|
||||
description: Timestamp when this token expires. Null if there is no expiry.
|
||||
description: Timestamp when this token expires. Null if there is no
|
||||
expiry.
|
||||
format: date-time
|
||||
last_activity:
|
||||
type: string
|
||||
@@ -2094,22 +2107,23 @@ components:
|
||||
tokenUrl: /hub/api/oauth2/token
|
||||
scopes:
|
||||
(no_scope): Identify the owner of the requesting entity.
|
||||
self: The user’s own resources _(metascope for users, resolves to (no_scope)
|
||||
for services)_
|
||||
inherit: Everything that the token-owning entity can access _(metascope
|
||||
for tokens)_
|
||||
admin-ui: Access the admin page. Permission to take actions via the admin
|
||||
page granted separately.
|
||||
admin:users: Read, modify, create, and delete users and their authentication
|
||||
state, not including their servers or tokens. This is an extremely privileged
|
||||
scope and should be considered tantamount to superuser.
|
||||
self: The user’s own resources _(metascope for users, resolves to
|
||||
(no_scope) for services)_
|
||||
inherit: Everything that the token-owning entity can access
|
||||
_(metascope for tokens)_
|
||||
admin-ui: Access the admin page. Permission to take actions via the
|
||||
admin page granted separately.
|
||||
admin:users: Read, modify, create, and delete users and their
|
||||
authentication state, not including their servers or tokens. This
|
||||
is an extremely privileged scope and should be considered
|
||||
tantamount to superuser.
|
||||
admin:auth_state: Read a user’s authentication state.
|
||||
users: Read and write permissions to user models (excluding servers, tokens
|
||||
and authentication state).
|
||||
users: Read and write permissions to user models (excluding servers,
|
||||
tokens and authentication state).
|
||||
delete:users: Delete users.
|
||||
list:users: List users, including at least their names.
|
||||
read:users: Read user models (including the URL of the default server
|
||||
if it is running).
|
||||
read:users: Read user models (including the URL of the default
|
||||
server if it is running).
|
||||
read:users:name: Read names of users.
|
||||
read:users:groups: Read users’ group membership.
|
||||
read:users:activity: Read time of last user activity.
|
||||
@@ -2118,24 +2132,25 @@ components:
|
||||
read:roles:services: Read service role assignments.
|
||||
read:roles:groups: Read group role assignments.
|
||||
users:activity: Update time of last user activity.
|
||||
admin:servers: Read, start, stop, create and delete user servers and their
|
||||
state.
|
||||
admin:servers: Read, start, stop, create and delete user servers and
|
||||
their state.
|
||||
admin:server_state: Read and write users’ server state.
|
||||
servers: Start and stop user servers.
|
||||
read:servers: Read users’ names and their server models (excluding the
|
||||
server state).
|
||||
read:servers: Read users’ names and their server models (excluding
|
||||
the server state).
|
||||
delete:servers: Stop and delete users' servers.
|
||||
tokens: Read, write, create and delete user tokens.
|
||||
read:tokens: Read user tokens.
|
||||
admin:groups: Read and write group information, create and delete groups.
|
||||
admin:groups: Read and write group information, create and delete
|
||||
groups.
|
||||
groups: 'Read and write group information, including adding/removing any
|
||||
users to/from groups. Note: adding users to groups may affect permissions.'
|
||||
list:groups: List groups, including at least their names.
|
||||
read:groups: Read group models.
|
||||
read:groups:name: Read group names.
|
||||
delete:groups: Delete groups.
|
||||
admin:services: Create, read, update, delete services, not including services
|
||||
defined from config files.
|
||||
admin:services: Create, read, update, delete services, not including
|
||||
services defined from config files.
|
||||
list:services: List services, including at least their names.
|
||||
read:services: Read service models.
|
||||
read:services:name: Read service names.
|
||||
@@ -2148,7 +2163,7 @@ components:
|
||||
read:groups:shares: Read servers shared with a group.
|
||||
read:shares: Read information about shared access to servers.
|
||||
shares: Manage access to shared servers.
|
||||
proxy: Read information about the proxy’s routing table, sync the Hub
|
||||
with the proxy and notify the Hub about a new proxy.
|
||||
proxy: Read information about the proxy’s routing table, sync the
|
||||
Hub with the proxy and notify the Hub about a new proxy.
|
||||
shutdown: Shutdown the hub.
|
||||
read:metrics: Read prometheus metrics.
|
||||
|
@@ -262,6 +262,7 @@ html_static_path = ["_static"]
|
||||
|
||||
html_theme = "jupyterhub_sphinx_theme"
|
||||
html_theme_options = {
|
||||
"header_links_before_dropdown": 6,
|
||||
"icon_links": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
@@ -297,7 +298,11 @@ linkcheck_ignore = [
|
||||
r"https://github.com/jupyterhub/jupyterhub/security/advisories/.*",
|
||||
# Occasionally blocks CI checks with 403
|
||||
r"https://www\.mysql\.com",
|
||||
r"https://www\.npmjs\.com",
|
||||
# Occasionally blocks CI checks with SSL error
|
||||
r"https://mediaspace\.msu\.edu/.*",
|
||||
]
|
||||
|
||||
linkcheck_anchors_ignore = [
|
||||
"/#!",
|
||||
"/#%21",
|
||||
|
@@ -2,19 +2,31 @@
|
||||
|
||||
# Community communication channels
|
||||
|
||||
```{note}
|
||||
Our community is distributed across the world in various timezones, so please be patient if you do not get a response immediately!
|
||||
```
|
||||
|
||||
We use different channels of communication for different purposes. Whichever one you use will depend on what kind of communication you want to engage in.
|
||||
|
||||
## Discourse (recommended)
|
||||
|
||||
We use [Discourse](https://discourse.jupyter.org) for online discussions and support questions.
|
||||
You can ask questions here if you are a first-time contributor to the JupyterHub project.
|
||||
Everyone in the Jupyter community is welcome to bring ideas and questions there.
|
||||
```{note}
|
||||
[Discourse] is open source.
|
||||
```
|
||||
|
||||
We recommend that you first use our Discourse as all past and current discussions on it are archived and searchable. Thus, all discussions remain useful and accessible to the whole community.
|
||||
We use [Jupyter instance of Discourse] for online discussions and support questions.
|
||||
You can ask questions at [Jupyter instance of Discourse] if you are a first-time contributor to the JupyterHub project.
|
||||
Everyone is welcome to bring ideas and questions at [Jupyter instance of Discourse].
|
||||
|
||||
## Gitter
|
||||
We recommend that you first use [Jupyter instance of Discourse] as all past and current discussions on it are archived and searchable. Thus, all discussions remain useful and accessible to the whole community.
|
||||
|
||||
We use [our Gitter channel](https://gitter.im/jupyterhub/jupyterhub) for online, real-time text chat; a place for more ephemeral discussions. When you're not on Discourse, you can stop here to have other discussions on the fly.
|
||||
## Zulip
|
||||
|
||||
```{note}
|
||||
[Zulip] is open source.
|
||||
```
|
||||
|
||||
We use [Jupyter instance of Zulip] for online, real-time text chat; a place for more ephemeral discussions. When you're not on [Jupyter instance of Discourse], you can stop at [Jupyter instance of Zulip] to have other discussions on the fly.
|
||||
|
||||
## Github Issues
|
||||
|
||||
@@ -24,6 +36,7 @@ We use [our Gitter channel](https://gitter.im/jupyterhub/jupyterhub) for online,
|
||||
- If you are using a specific JupyterHub distribution (such as [Zero to JupyterHub on Kubernetes](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) or [The Littlest JupyterHub](https://github.com/jupyterhub/the-littlest-jupyterhub/)), you should open issues directly in their repository.
|
||||
- If you cannot find a repository to open your issue in, do not worry! Open the issue in the [main JupyterHub repository](https://github.com/jupyterhub/jupyterhub/) and our community will help you figure it out.
|
||||
|
||||
```{note}
|
||||
Our community is distributed across the world in various timezones, so please be patient if you do not get a response immediately!
|
||||
```
|
||||
[Discourse]: https://www.discourse.org/
|
||||
[Jupyter instance of Discourse]: https://discourse.jupyter.org
|
||||
[Jupyter instance of Zulip]: https://jupyter.zulipchat.com/
|
||||
[Zulip]: https://zulip.com/
|
||||
|
@@ -5,49 +5,42 @@
|
||||
Documentation is often more important than code. This page helps
|
||||
you get set up on how to contribute to JupyterHub's documentation.
|
||||
|
||||
We use [Sphinx](https://www.sphinx-doc.org) to build our documentation. It takes
|
||||
our documentation source files (written in [Markedly Structured Text (MyST)](https://mystmd.org/) and
|
||||
stored under the `docs/source` directory) and converts it into various
|
||||
formats for people to read.
|
||||
|
||||
## Building documentation locally
|
||||
|
||||
We use [sphinx](https://www.sphinx-doc.org) to build our documentation. It takes
|
||||
our documentation source files (written in [markdown](https://daringfireball.net/projects/markdown/) or [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) &
|
||||
stored under the `docs/source` directory) and converts it into various
|
||||
formats for people to read. To make sure the documentation you write or
|
||||
To make sure the documentation you write or
|
||||
change renders correctly, it is good practice to test it locally.
|
||||
|
||||
1. Make sure you have successfully completed {ref}`contributing:setup`.
|
||||
```{note}
|
||||
You will need Python and Git installed. Installation details are avaiable at {ref}`contributing:setup`.
|
||||
```
|
||||
|
||||
2. Install the packages required to build the docs.
|
||||
1. Install the packages required to build the docs.
|
||||
|
||||
```bash
|
||||
python3 -m pip install -r docs/requirements.txt
|
||||
python3 -m pip install sphinx-autobuild
|
||||
```
|
||||
|
||||
3. Build the html version of the docs. This is the most commonly used
|
||||
2. Build the HTML version of the docs. This is the most commonly used
|
||||
output format, so verifying it renders correctly is usually good
|
||||
enough.
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
make html
|
||||
sphinx-autobuild docs/source/ docs/_build/html
|
||||
```
|
||||
|
||||
This step will display any syntax or formatting errors in the documentation,
|
||||
along with the filename / line number in which they occurred. Fix them,
|
||||
and re-run the `make html` command to re-render the documentation.
|
||||
and the HTML will be re-render automatically.
|
||||
|
||||
4. View the rendered documentation by opening `_build/html/index.html` in
|
||||
3. View the rendered documentation by opening <http://127.0.0.1:8000> in
|
||||
a web browser.
|
||||
|
||||
:::{tip}
|
||||
**On Windows**, you can open a file from the terminal with `start <path-to-file>`.
|
||||
|
||||
**On macOS**, you can do the same with `open <path-to-file>`.
|
||||
|
||||
**On Linux**, you can do the same with `xdg-open <path-to-file>`.
|
||||
|
||||
After opening index.html in your browser you can just refresh the page whenever
|
||||
you rebuild the docs via `make html`
|
||||
:::
|
||||
|
||||
(contributing-docs-conventions)=
|
||||
|
||||
## Documentation conventions
|
||||
@@ -67,10 +60,10 @@ approach:
|
||||
python3 -m pip
|
||||
```
|
||||
|
||||
This invokes pip explicitly using the python3 binary that you are
|
||||
This invokes `pip` explicitly using the `python3` binary that you are
|
||||
currently using. This is the **recommended way** to invoke pip
|
||||
in our documentation, since it is least likely to cause problems
|
||||
with python3 and pip being from different environments.
|
||||
with `python3` and `pip` being from different environments.
|
||||
|
||||
For more information on how to invoke `pip` commands, see
|
||||
[the pip documentation](https://pip.pypa.io/en/stable/).
|
||||
[the `pip` documentation](https://pip.pypa.io/en/stable/).
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# Contributing
|
||||
|
||||
We want you to contribute to JupyterHub in ways that are most exciting
|
||||
and useful to you. We value documentation, testing, bug reporting & code equally,
|
||||
and useful to you. We value documentation, testing, bug reporting and code equally,
|
||||
and are glad to have your contributions in whatever form you wish.
|
||||
|
||||
Be sure to first check our [Code of Conduct](https://github.com/jupyter/governance/blob/HEAD/conduct/code_of_conduct.md)
|
||||
|
@@ -5,7 +5,11 @@
|
||||
If you find a security vulnerability in Jupyter or JupyterHub,
|
||||
whether it is a failure of the security model described in [Security Overview](explanation:security)
|
||||
or a failure in implementation,
|
||||
please report it to <mailto:security@ipython.org>.
|
||||
please report it!
|
||||
|
||||
Please use GitHub's "Report a Vulnerability" button under Security > Advisories on the appropriate repo,
|
||||
e.g. [report here for JupyterHub](https://github.com/jupyterhub/jupyterhub/security/advisories).
|
||||
|
||||
You may also send an email to <mailto:security@ipython.org>, but the GitHub reporting system is preferred.
|
||||
If you prefer to encrypt your security reports,
|
||||
you can use {download}`this PGP public key </ipython_security.asc>`.
|
||||
|
@@ -2,37 +2,56 @@
|
||||
|
||||
# Setting up a development install
|
||||
|
||||
JupyterHub's continuous integration runs on [Ubuntu LTS](https://ubuntu.com/).
|
||||
|
||||
While JupyterHub is only tested on one [Linux distribution](https://en.wikipedia.org/wiki/Linux_distribution),
|
||||
it should be fairly insensitive to variations between common [POXIS](https://en.wikipedia.org/wiki/POSIX) implementation,
|
||||
though we don't have the bandwidth to verify this automatically and continuously.
|
||||
|
||||
Feel free to try it on your platform, and be sure to {ref}`let us know <contributing:community>` about any issues you encounter.
|
||||
|
||||
## System requirements
|
||||
|
||||
JupyterHub can only run on macOS or Linux operating systems. If you are
|
||||
using Windows, we recommend using [VirtualBox](https://virtualbox.org)
|
||||
or a similar system to run [Ubuntu Linux](https://ubuntu.com) for
|
||||
development.
|
||||
Your system **must** be able to run
|
||||
|
||||
- Python
|
||||
- NodeJS
|
||||
- Git
|
||||
|
||||
Our small team knows JupyterHub to work perfectly on macOS or Linux operating systems.
|
||||
|
||||
```{admonition} What about Windows?
|
||||
Some users have reported that JupyterHub runs successfully on [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/). We have no plans to support Windows outside of the WSL.
|
||||
```
|
||||
|
||||
```{admonition} What about virtualization?
|
||||
Using any form of virtualization (for example, [VirtualBox](https://www.virtualbox.org/), [Docker](https://www.docker.com/), [Podman](https://podman.io/), [WSL](https://learn.microsoft.com/en-us/windows/wsl/)) is a good way to get up and running quickly, though properly configuring the networking settings can be a bit tricky.
|
||||
```
|
||||
|
||||
### Install Python
|
||||
|
||||
JupyterHub is written in the [Python](https://python.org) programming language and
|
||||
JupyterHub is written in the [Python](https://www.python.org) programming language and
|
||||
requires you have at least version {{python_min}} installed locally. If you haven’t
|
||||
installed Python before, the recommended way to install it is to use
|
||||
[Miniforge](https://github.com/conda-forge/miniforge#download).
|
||||
|
||||
### Install nodejs
|
||||
### Install NodeJS
|
||||
|
||||
[NodeJS {{node_min}}+](https://nodejs.org/en/) is required for building some JavaScript components.
|
||||
`configurable-http-proxy`, the default proxy implementation for JupyterHub, is written in Javascript.
|
||||
Some JavaScript components require you have at least version {{node_min}} of [NodeJS](https://nodejs.org/en/) installed locally.
|
||||
`configurable-http-proxy`, the default proxy implementation for JupyterHub, is written in JavaScript.
|
||||
If you have not installed NodeJS before, we recommend installing it in the `miniconda` environment you set up for Python.
|
||||
You can do so with `conda install nodejs`.
|
||||
|
||||
Many in the Jupyter community use [`nvm`](https://github.com/nvm-sh/nvm) to
|
||||
managing node dependencies.
|
||||
|
||||
### Install git
|
||||
### Install Git
|
||||
|
||||
JupyterHub uses [Git](https://git-scm.com) & [GitHub](https://github.com)
|
||||
for development & collaboration. You need to [install git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) to work on
|
||||
JupyterHub. We also recommend getting a free account on GitHub.com.
|
||||
JupyterHub uses [Git](https://git-scm.com) and [GitHub](https://github.com)
|
||||
for development and collaboration. You need to [install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) to work on
|
||||
JupyterHub. We also recommend getting a free account on GitHub.
|
||||
|
||||
## Setting up a development install
|
||||
## Install JupyterHub for development
|
||||
|
||||
When developing JupyterHub, you would need to make changes and be able to instantly view the results of the changes. To achieve that, a developer install is required.
|
||||
|
||||
@@ -44,7 +63,7 @@ be achieved in many ways, for example, `tox`, `conda`, `docker`, etc. See this
|
||||
a more detailed discussion.
|
||||
:::
|
||||
|
||||
1. Clone the [JupyterHub git repository](https://github.com/jupyterhub/jupyterhub)
|
||||
1. Clone the [JupyterHub Git repository](https://github.com/jupyterhub/jupyterhub)
|
||||
to your computer.
|
||||
|
||||
```bash
|
||||
@@ -65,7 +84,7 @@ a more detailed discussion.
|
||||
npm -v
|
||||
```
|
||||
|
||||
This should return a version number greater than or equal to 5.0.
|
||||
This should return a version number greater than or equal to {{node_min}}.
|
||||
|
||||
3. Install `configurable-http-proxy` (required to run and test the default JupyterHub configuration):
|
||||
|
||||
@@ -92,7 +111,7 @@ a more detailed discussion.
|
||||
|
||||
4. Install an editable version of JupyterHub and its requirements for
|
||||
development and testing. This lets you edit JupyterHub code in a text editor
|
||||
& restart the JupyterHub process to see your code changes immediately.
|
||||
and restart the JupyterHub process to see your code changes immediately.
|
||||
|
||||
```bash
|
||||
python3 -m pip install --editable ".[test]"
|
||||
@@ -109,7 +128,7 @@ a more detailed discussion.
|
||||
|
||||
Happy developing!
|
||||
|
||||
## Using DummyAuthenticator & SimpleLocalProcessSpawner
|
||||
## Using DummyAuthenticator and SimpleLocalProcessSpawner
|
||||
|
||||
To simplify testing of JupyterHub, it is helpful to use
|
||||
{class}`~jupyterhub.auth.DummyAuthenticator` instead of the default JupyterHub
|
||||
@@ -132,17 +151,17 @@ The test configuration enables a few things to make testing easier:
|
||||
- disable caching of static files
|
||||
|
||||
The default JupyterHub [authenticator](PAMAuthenticator)
|
||||
& [spawner](LocalProcessSpawner)
|
||||
and [spawner](LocalProcessSpawner)
|
||||
require your system to have user accounts for each user you want to log in to
|
||||
JupyterHub as.
|
||||
|
||||
DummyAuthenticator allows you to log in with any username & password,
|
||||
DummyAuthenticator allows you to log in with any username and password,
|
||||
while SimpleLocalProcessSpawner allows you to start servers without having to
|
||||
create a Unix user for each JupyterHub user. Together, these make it
|
||||
much easier to test JupyterHub.
|
||||
|
||||
Tip: If you are working on parts of JupyterHub that are common to all
|
||||
authenticators & spawners, we recommend using both DummyAuthenticator &
|
||||
authenticators and spawners, we recommend using both DummyAuthenticator and
|
||||
SimpleLocalProcessSpawner. If you are working on just authenticator-related
|
||||
parts, use only SimpleLocalProcessSpawner. Similarly, if you are working on
|
||||
just spawner-related parts, use only DummyAuthenticator.
|
||||
|
@@ -6,61 +6,69 @@ Unit testing helps to validate that JupyterHub works the way we think it does,
|
||||
and continues to do so when changes occur. They also help communicate
|
||||
precisely what we expect our code to do.
|
||||
|
||||
JupyterHub uses [pytest](https://pytest.org) for all the tests. You
|
||||
can find them under the [jupyterhub/tests](https://github.com/jupyterhub/jupyterhub/tree/main/jupyterhub/tests) directory in the git repository.
|
||||
JupyterHub uses [`pytest`](https://pytest.org) for all the tests. You
|
||||
can find them under the [jupyterhub/tests](https://github.com/jupyterhub/jupyterhub/tree/main/jupyterhub/tests) directory in the Git repository.
|
||||
|
||||
## Running the tests
|
||||
```{note}
|
||||
Before run any test, make sure you have completed {ref}`contributing:setup`.
|
||||
Once you are done, you would be able to run `jupyterhub` from the command line and access it from your web browser.
|
||||
This ensures that the development environment is properly set up for tests to run.
|
||||
```
|
||||
|
||||
1. Make sure you have completed {ref}`contributing:setup`.
|
||||
Once you are done, you would be able to run `jupyterhub` from the command line and access it from your web browser.
|
||||
This ensures that the dev environment is properly set up for tests to run.
|
||||
```{note}
|
||||
For details of `pytest`, refer to the [`pytest` usage documentation](https://pytest.readthedocs.io/en/latest/usage.html).
|
||||
```
|
||||
|
||||
2. You can run all tests in JupyterHub
|
||||
## Running all the tests
|
||||
|
||||
```bash
|
||||
pytest -v jupyterhub/tests
|
||||
```
|
||||
You can run all tests in JupyterHub
|
||||
|
||||
This should display progress as it runs all the tests, printing
|
||||
information about any test failures as they occur.
|
||||
```bash
|
||||
pytest -v jupyterhub/tests
|
||||
```
|
||||
|
||||
If you wish to confirm test coverage the run tests with the `--cov` flag:
|
||||
This should display progress as it runs all the tests, printing
|
||||
information about any test failures as they occur.
|
||||
|
||||
```bash
|
||||
pytest -v --cov=jupyterhub jupyterhub/tests
|
||||
```
|
||||
If you wish to confirm test coverage the run tests with the `--cov` flag:
|
||||
|
||||
3. You can also run tests in just a specific file:
|
||||
```bash
|
||||
pytest -v --cov=jupyterhub jupyterhub/tests
|
||||
```
|
||||
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/<test-file-name>
|
||||
```
|
||||
## Running tests from a specific file
|
||||
|
||||
4. To run a specific test only, you can do:
|
||||
You can also run tests in just a specific file:
|
||||
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/<test-file-name>::<test-name>
|
||||
```
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/<test-file-name>
|
||||
```
|
||||
|
||||
This runs the test with function name `<test-name>` defined in
|
||||
`<test-file-name>`. This is very useful when you are iteratively
|
||||
developing a single test.
|
||||
## Running a single test
|
||||
|
||||
For example, to run the test `test_shutdown` in the file `test_api.py`,
|
||||
you would run:
|
||||
To run a specific test only, you can do:
|
||||
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/test_api.py::test_shutdown
|
||||
```
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/<test-file-name>::<test-name>
|
||||
```
|
||||
|
||||
For more details, refer to the [pytest usage documentation](https://pytest.readthedocs.io/en/latest/usage.html).
|
||||
This runs the test with function name `<test-name>` defined in
|
||||
`<test-file-name>`. This is very useful when you are iteratively
|
||||
developing a single test.
|
||||
|
||||
For example, to run the test `test_shutdown` in the file `test_api.py`,
|
||||
you would run:
|
||||
|
||||
```bash
|
||||
pytest -v jupyterhub/tests/test_api.py::test_shutdown
|
||||
```
|
||||
|
||||
## Test organisation
|
||||
|
||||
The tests live in `jupyterhub/tests` and are organized roughly into:
|
||||
|
||||
1. `test_api.py` tests the REST API
|
||||
2. `test_pages.py` tests loading the HTML pages
|
||||
1. `test_api.py`: tests the REST API
|
||||
2. `test_pages.py`: tests loading the HTML pages
|
||||
|
||||
and other collections of tests for different components.
|
||||
When writing a new test, there should usually be a test of
|
||||
@@ -126,7 +134,7 @@ For more information on asyncio and event-loops, here are some resources:
|
||||
|
||||
### All the tests are failing
|
||||
|
||||
Make sure you have completed all the steps in {ref}`contributing:setup` successfully, and are able to access JupyterHub from your browser at http://localhost:8000 after starting `jupyterhub` in your command line.
|
||||
Make sure you have completed all the steps in {ref}`contributing:setup` successfully, and are able to access JupyterHub from your browser at <http://localhost:8000> after starting `jupyterhub` in your command line.
|
||||
|
||||
## Code formatting and linting
|
||||
|
||||
|
@@ -108,26 +108,29 @@ Doing so generally involves:
|
||||
### Default backend: SQLite
|
||||
|
||||
The default database backend for JupyterHub is [SQLite](https://sqlite.org).
|
||||
We have chosen SQLite as JupyterHub's default because it's simple (the 'database' is a single file) and ubiquitous (it is in the Python standard library).
|
||||
It works very well for testing, small deployments, and workshops.
|
||||
We have chosen SQLite as JupyterHub's default because it's simple (the 'database' is a single file), ubiquitous (it is in the Python standard library), and it does not require maintaining a separate database server.
|
||||
|
||||
For production systems, SQLite has some disadvantages when used with JupyterHub:
|
||||
The main disadvantage of SQLite is it does not support remote backup tools or replication.
|
||||
You should backup your database by taking snapshots of the file (`jupyterhub.sqlite`).
|
||||
|
||||
- `upgrade-db` may not always work, and you may need to start with a fresh database
|
||||
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
||||
version, so backup the `jupyterhub.sqlite` file before upgrading (JupyterHub automatically creates a date-stamped backup file when upgrading sqlite)
|
||||
SQLite is ideal for testing, small deployments, workshops, and production servers where you do not require remote backup or replication.
|
||||
|
||||
### Picking your database backend (PostgreSQL, MySQL)
|
||||
|
||||
The sqlite documentation provides a helpful page about [when to use SQLite and
|
||||
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
||||
|
||||
### Picking your database backend (PostgreSQL, MySQL)
|
||||
|
||||
When running a long term deployment or a production system, we recommend using a full-fledged relational database, such as [PostgreSQL](https://www.postgresql.org) or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE` statement, which is used in some database upgrade steps.
|
||||
|
||||
In general, you select your database backend with [](JupyterHub.db_url), and can further configure it (usually not necessary) with [](JupyterHub.db_kwargs).
|
||||
|
||||
## Notes and Tips
|
||||
|
||||
### Upgrading the JupyterHub database
|
||||
|
||||
[Upgrading JupyterHub to a new major release](howto:upgrading-jupyterhub) often requires an upgrade to the database schema.
|
||||
|
||||
- `jupyterhub upgrade-db` will execute a schema upgrade. You should backup your database before running this.
|
||||
- `jupyterhub downgrade-db` may be able to revert a schema upgrade on PostgreSQL and MySQL, but this is not guaranteed to work, and is not supported.
|
||||
|
||||
### SQLite
|
||||
|
||||
The SQLite database should not be used on NFS. SQLite uses reader/writer locks
|
||||
|
@@ -98,7 +98,7 @@ the OAuth callback request.
|
||||
to retrieve information about the owner of the token (the user).
|
||||
This is the step where behavior diverges for different OAuth providers.
|
||||
Up to this point, all OAuth providers are the same, following the OAuth specification.
|
||||
However, OAuth does not define a standard for issuing tokens in exchange for information about their owner or permissions ([OpenID Connect](https://openid.net/connect/) does that),
|
||||
However, OAuth does not define a standard for issuing tokens in exchange for information about their owner or permissions ([OpenID Connect](https://openid.net/developers/how-connect-works/) does that),
|
||||
so this step may be different for each OAuth provider.
|
||||
- Finally, the OAuth client stores its own record that the user is authorized in a cookie.
|
||||
This could be the token itself, or any other appropriate representation of successful authentication.
|
||||
|
@@ -101,7 +101,7 @@ matching `*.jupyter.example.org`.
|
||||
Unfortunately, for many institutional domains, wildcard DNS and SSL may not be available.
|
||||
|
||||
We also **strongly encourage** serving JupyterHub and user content on a domain that is _not_ a subdomain of any sensitive content.
|
||||
For reasoning, see [GitHub's discussion of moving user content to github.io from \*.github.com](https://github.blog/2013-04-09-yummy-cookies-across-domains/).
|
||||
For reasoning, see [GitHub's discussion of moving user content to github.io from \*.github.com](https://github.blog/engineering/yummy-cookies-across-domains/).
|
||||
|
||||
**If you do plan to serve untrusted users, enabling subdomains is highly encouraged**,
|
||||
as it resolves many security issues, which are difficult to unavoidable when JupyterHub is on a single-domain.
|
||||
@@ -186,7 +186,6 @@ For example:
|
||||
|
||||
- `Content-Security-Policy` header must prohibit popups and iframes from the same origin.
|
||||
The following Content-Security-Policy rules are _insecure_ and readily enable users to access each others' servers:
|
||||
|
||||
- `frame-ancestors: 'self'`
|
||||
- `frame-ancestors: '*'`
|
||||
- `sandbox allow-popups`
|
||||
|
@@ -142,7 +142,7 @@ in a variety of deployment setups. This often entails connecting your JupyterHub
|
||||
in these cases, and the security of your JupyterHub deployment will often depend on these decisions.
|
||||
|
||||
If you are worried about security, don't hesitate to reach out to the JupyterHub community in the
|
||||
[Jupyter Community Forum](https://discourse.jupyter.org/c/jupyterhub). This community of practice has many
|
||||
[Jupyter Community Forum](https://discourse.jupyter.org/c/jupyterhub/10). This community of practice has many
|
||||
individuals with experience running secure JupyterHub deployments and will be very glad to help you out.
|
||||
|
||||
### Does JupyterHub provide computing or data infrastructure?
|
||||
|
@@ -35,7 +35,7 @@ This user shouldn't have a login shell or password (possible with -r).
|
||||
|
||||
## Set up sudospawner
|
||||
|
||||
Next, you will need [sudospawner](https://github.com/jupyter/sudospawner)
|
||||
Next, you will need [sudospawner](https://github.com/jupyterhub/sudospawner)
|
||||
to enable monitoring the single-user servers with sudo:
|
||||
|
||||
```bash
|
||||
@@ -72,7 +72,7 @@ rhea ALL=(JUPYTER_USERS) NOPASSWD:JUPYTER_CMD
|
||||
```
|
||||
|
||||
It might be useful to modify `secure_path` to add commands in path. (Search for
|
||||
`secure_path` in the [sudo docs](https://www.sudo.ws/man/1.8.14/sudoers.man.html)
|
||||
`secure_path` in the [sudo docs](https://www.sudo.ws)
|
||||
|
||||
As an alternative to adding every user to the `/etc/sudoers` file, you can
|
||||
use a group in the last line above, instead of `JUPYTER_USERS`:
|
||||
|
130
docs/source/howto/forced-login.md
Normal file
130
docs/source/howto/forced-login.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Logging users in via URL
|
||||
|
||||
Sometimes, JupyterHub is integrated into an existing application that has already handled user login, etc..
|
||||
It is often preferable in these applications to be able to link users to their running JupyterHub server without _prompting_ the user to login again with the Hub when the Hub should really be an implementation detail,
|
||||
and not part of the user experience.
|
||||
|
||||
One way to do this has been to use [API only mode](#howto:api-only), issue tokens for users, and redirect users to a URL like `/users/name/?token=abc123`.
|
||||
This is [disabled by default](#HubAuth.allow_token_in_url) in JupyterHub 5, because it presents a vulnerability for users to craft links that let _other_ users login as them, which can lead to inter-user attacks.
|
||||
|
||||
But that leaves the question: how do I as an _application developer_ embedding JupyterHub link users to their own running server without triggering another login prompt?
|
||||
|
||||
The problem with `?token=...` in the URL is specifically that _users_ can get and create these tokens, and share URLs.
|
||||
This wouldn't be an issue if only authorized applications could issue tokens that behave this way.
|
||||
The single-user server doesn't exactly have the hooks to manage this easily, but the [Authenticator](#Authenticator) API does.
|
||||
|
||||
## Problem statement
|
||||
|
||||
We want our external application to be able to:
|
||||
|
||||
1. authenticate users
|
||||
2. (maybe) create JupyterHub users
|
||||
3. start JupyterHub servers
|
||||
4. redirect users into running servers _without_ any login prompts/loading pages from JupyterHub, and without any prior JupyterHub credentials
|
||||
|
||||
Step 1 is up to the application and not JupyterHub's problem.
|
||||
Step 2 and 3 use the JupyterHub [REST API](#jupyterhub-rest-API).
|
||||
The service would need the scopes:
|
||||
|
||||
```
|
||||
admin:users # creating users
|
||||
servers # start/stop servers
|
||||
```
|
||||
|
||||
That leaves the last step: sending users to their running server with credentials, without prompting login.
|
||||
This is where things can get tricky!
|
||||
|
||||
### Ideal case: oauth
|
||||
|
||||
_Ideally_, the best way to set this up is with the external service as an OAuth provider,
|
||||
though in some cases it works best to use proxy-based authentication like Shibboleth / [REMOTE_USER](https://github.com/cwaldbieser/jhub_remote_user_authenticator).
|
||||
The main things to know are:
|
||||
|
||||
- Links to `/hub/user-redirect/some/path` will ultimately land users at `/users/theirserver/some/path` after completing login, ensuring the server is running, etc.
|
||||
- Setting `Authenticator.auto_login = True` allows beginning the login process without JupyterHub's "Login with..." prompt
|
||||
|
||||
_If_ your OAuth provider allows logging in to external services via your oauth provider without prompting, this is enough.
|
||||
Not all do, though.
|
||||
|
||||
If you've already ensured the server is running, this will _appear_ to the user as if they are being sent directly to their running server.
|
||||
But what _actually_ happens is quite a series of redirects, state checks, and cookie-setting:
|
||||
|
||||
1. visiting `/hub/user-redirect/some/path` checks if the user is logged in
|
||||
1. if not, begin the login process (`/hub/login?next=/hub/user-redirect/...`)
|
||||
2. redirects to your oauth provider to authenticate the user
|
||||
3. redirects back to `/hub/oauth_callback` to complete login
|
||||
4. redirects back to `/hub/user-redirect/...`
|
||||
2. once authenticated, checks that the user's server is running
|
||||
1. if not running, begins launch of the server
|
||||
2. redirects to `/hub/spawn-pending/?next=...`
|
||||
3. once the server is running, redirects to the actual user server `/users/username/some/path`
|
||||
|
||||
Now we're done, right? Actually, no, because the browser doesn't have credentials for their user server!
|
||||
This sequence of redirects happens all the time in JupyterHub launch, and is usually totally transparent.
|
||||
|
||||
4. at the user server, check for a token in cookie
|
||||
1. if not present or not valid, begin oauth with the Hub (redirect to `/hub/api/oauth2/authorize/...`)
|
||||
2. hub redirects back to `/users/user/oauth_callback` to complete oauth
|
||||
3. redirect again to the URL that started this internal oauth
|
||||
5. finally, arrive at `/users/username/some/path`, the ultimate destination, with valid JupyterHub credentials
|
||||
|
||||
The steps that will show users something other than the page you want them to are:
|
||||
|
||||
- Step 1.1 will be a prompt e.g. with "Login with..." unless you set `c.Authenticator.auto_login = True`
|
||||
- Step 1.2 _may_ be a prompt from your oauth provider. This isn't controlled by JupyterHub, and may not be avoidable.
|
||||
- Step 2.2 will show the spawn pending page only if the server is not already running
|
||||
|
||||
Otherwise, this is all transparent redirects to the final destination.
|
||||
|
||||
#### Using an authentication proxy (REMOTE_USER)
|
||||
|
||||
If you use an Authentication proxy like Shibboleth that sets e.g. the REMOTE_USER header,
|
||||
you can use an Authenticator like [RemoteUserAuthenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator) to automatically login users based on headers in the request.
|
||||
The same process will work, but instead of step 1.1 redirecting to the oauth provider, it logs in immediately.
|
||||
If you do support an auth proxy, you also need to be extremely sure that requests only come from the auth proxy, and don't accept any requests setting the REMOTE_USER header coming from other sources.
|
||||
|
||||
### Custom case
|
||||
|
||||
But let's say you can't use OAuth or REMOTE_USER, and you still want to hide JupyterHub implementation details.
|
||||
All you really want is a way to write a URL that will take users to their servers without any login prompts.
|
||||
|
||||
You can do this if you create an Authenticator with `auto_login=True` that logs users in based on something in the _request_, e.g. a query parameter.
|
||||
|
||||
We have an _example_ in the JupyterHub repo in `examples/forced-login` that does this.
|
||||
It is a sample 'external service' where you type in a username and a destination path.
|
||||
When you 'login' with this username:
|
||||
|
||||
1. a token is issued
|
||||
2. the token is stored and associated with the username
|
||||
3. redirect to `/hub/login?login_token=...&next=/hub/user-redirect/destination/path`
|
||||
|
||||
Then on the JupyterHub side, there is the `ForcedLoginAuthenticator`.
|
||||
This class implements `authenticate`, which:
|
||||
|
||||
1. has `auto_login = True` so visiting `/hub/login` calls `authenticate()` directly instead of serving a page
|
||||
2. gets the token from the `login_token` URL parameter
|
||||
3. makes a POST request to the external application with the token, requesting a username
|
||||
4. the external application returns the username and deletes the token, so it cannot be re-used
|
||||
5. Authenticator returns the username
|
||||
|
||||
This doesn't _bypass_ JupyterHub authentication, as some deployments have done, but it does _hide_ it.
|
||||
If your service launches servers via the API, you could run this in [API only mode](#howto:api-only) by adding `/hub/login` as well:
|
||||
|
||||
```python
|
||||
c.JupyterHub.hub_routespec = "/hub/api/"
|
||||
c.Proxy.additional_routes = {"/hub/login": "http://hub:8080"}
|
||||
```
|
||||
|
||||
```{literalinclude} ../../../examples/forced-login/jupyterhub_config.py
|
||||
:language: python
|
||||
:start-at: class ForcedLoginAuthenticator
|
||||
:end-before: c = get_config()
|
||||
```
|
||||
|
||||
**Why does this work?**
|
||||
|
||||
This is still logging in with a token in the URL, right?
|
||||
Yes, but the key difference is that users cannot issue these tokens.
|
||||
The sample application is still technically vulnerable, because the token link should really be non-transferrable, even if it can only be used once.
|
||||
The only defense the sample application has against this is rapidly expiring tokens (they expire after 30 seconds).
|
||||
You can use state cookies, etc. to manage that more rigorously, as done in OAuth (at which point, maybe implement OAuth itself, why not?).
|
@@ -14,7 +14,7 @@ separate-proxy
|
||||
templates
|
||||
upgrading
|
||||
log-messages
|
||||
|
||||
forced-login
|
||||
```
|
||||
|
||||
(config-examples)=
|
||||
|
@@ -71,4 +71,4 @@ aligned, rather than as an indicator of an existing problem.
|
||||
Upgrade the version of the `jupyterhub` package in your user environment or image
|
||||
so that it matches the version of JupyterHub running your JupyterHub server! If you
|
||||
are using the [zero-to-jupyterhub](https://z2jh.jupyter.org) helm chart, you can find the appropriate
|
||||
version of the `jupyterhub` package to install in your user image [here](https://jupyterhub.github.io/helm-chart/)
|
||||
version of the `jupyterhub` package to install in your user image [here](https://hub.jupyter.org/helm-chart/)
|
||||
|
@@ -232,4 +232,4 @@ A list of the proxies that are currently available for JupyterHub (that we know
|
||||
|
||||
1. [`jupyterhub/configurable-http-proxy`](https://github.com/jupyterhub/configurable-http-proxy) The default proxy which uses node-http-proxy
|
||||
2. [`jupyterhub/traefik-proxy`](https://github.com/jupyterhub/traefik-proxy) The proxy which configures traefik proxy server for jupyterhub
|
||||
3. [`AbdealiJK/configurable-http-proxy`](https://github.com/AbdealiJK/configurable-http-proxy) A pure python implementation of the configurable-http-proxy
|
||||
3. [`AbdealiJK/configurable-http-proxy`](https://github.com/corridor/configurable-http-proxy) A pure python implementation of the configurable-http-proxy
|
||||
|
@@ -201,7 +201,7 @@ Authorization header.
|
||||
|
||||
### Use requests
|
||||
|
||||
Using the popular Python [requests](https://docs.python-requests.org)
|
||||
Using the popular Python [requests](https://requests.readthedocs.io)
|
||||
library, an API GET request is made to [/users](rest-api-get-users), and the request sends an API token for
|
||||
authorization. The response contains information about the users, here's example code to make an API request for the users of a JupyterHub deployment
|
||||
|
||||
|
@@ -27,7 +27,7 @@ For specific version migrations:
|
||||
The [changelog](changelog) contains information on what has
|
||||
changed with the new JupyterHub release and any deprecation warnings.
|
||||
Read these notes to familiarize yourself with the coming changes. There
|
||||
might be new releases of the authenticators & spawners you use, so
|
||||
might be new releases of the authenticators and spawners you use, so
|
||||
read the changelogs for those too!
|
||||
|
||||
## Notify your users
|
||||
@@ -41,7 +41,7 @@ If you use a different proxy or run `configurable-http-proxy`
|
||||
independent of JupyterHub, your users will be able to continue using notebook
|
||||
servers they had already launched, but will not be able to launch new servers or sign in.
|
||||
|
||||
## Backup database & config
|
||||
## Backup database and config
|
||||
|
||||
Before doing an upgrade, it is critical to back up:
|
||||
|
||||
@@ -90,7 +90,7 @@ with:
|
||||
conda install -c conda-forge jupyterhub==<version>
|
||||
```
|
||||
|
||||
You should also check for new releases of the authenticator & spawner you
|
||||
You should also check for new releases of the authenticator and spawner you
|
||||
are using. You might wish to upgrade those packages, too, along with JupyterHub
|
||||
or upgrade them separately.
|
||||
|
||||
@@ -107,17 +107,6 @@ jupyterhub upgrade-db
|
||||
This should find the location of your database, and run the necessary upgrades
|
||||
for it.
|
||||
|
||||
### SQLite database disadvantages
|
||||
|
||||
SQLite has some disadvantages when it comes to upgrading JupyterHub. These
|
||||
are:
|
||||
|
||||
- `upgrade-db` may not work, and you may need to delete your database
|
||||
and start with a fresh one.
|
||||
- `downgrade-db` **will not** work if you want to rollback to an
|
||||
earlier version, so backup the `jupyterhub.sqlite` file before
|
||||
upgrading.
|
||||
|
||||
### What happens if I delete my database?
|
||||
|
||||
Losing the Hub database is often not a big deal. Information that
|
||||
|
@@ -17,7 +17,7 @@ It has two main distributions which are developed to serve the needs of each of
|
||||
|
||||
1. [The Littlest JupyterHub](https://github.com/jupyterhub/the-littlest-jupyterhub) distribution is suitable if you need a small number of users (1-100) and a single server with a simple environment.
|
||||
2. [Zero to JupyterHub with Kubernetes](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) allows you to deploy dynamic servers on the cloud if you need even more users.
|
||||
This distribution runs JupyterHub on top of [Kubernetes](https://k8s.io).
|
||||
This distribution runs JupyterHub on top of [Kubernetes](https://kubernetes.io/).
|
||||
|
||||
```{note}
|
||||
It is important to evaluate these distributions before you can continue with the
|
||||
|
@@ -84,7 +84,6 @@ The passed scopes are compared to the scopes required to access the API as follo
|
||||
- if the API scopes are present within the set of passed scopes, the access is granted and the API returns its "full" response
|
||||
|
||||
- if that is not the case, another check is utilized to determine if subscopes of the required API scopes can be found in the passed scope set:
|
||||
|
||||
- if found, the RBAC framework employs the {ref}`filtering <vertical-filtering-target>` procedures to refine the API response to access only resource attributes corresponding to the passed scopes. For example, providing a scope `read:users:activity!group=class-C` for the `GET /users` API will return a list of user models from group `class-C` containing only the `last_activity` attribute for each user model
|
||||
|
||||
- if not found, the access to API is denied
|
||||
|
@@ -20,6 +20,74 @@ Contributors to major version bumps in JupyterHub include:
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 5.4
|
||||
|
||||
### 5.4.0 - 2025-10-06
|
||||
|
||||
JupyterHub 5.4 is a small release with a few new features and several nice bugfixes.
|
||||
No special upgrade steps should be required.
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/5.3.0...5.4.0))
|
||||
|
||||
#### New features added
|
||||
|
||||
- Add Authenticator.refresh_pre_stop option [#5067](https://github.com/jupyterhub/jupyterhub/pull/5067) ([@kreuzert](https://github.com/kreuzert), [@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Enhancements made
|
||||
|
||||
- Add confirmation dialog for named server deletion [#5093](https://github.com/jupyterhub/jupyterhub/pull/5093) ([@kateryna-tarelkina-dd](https://github.com/kateryna-tarelkina-dd), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- make sure internal ssl works with pycurl [#5164](https://github.com/jupyterhub/jupyterhub/pull/5164) ([@minrk](https://github.com/minrk), [@kreuzert](https://github.com/kreuzert))
|
||||
- don't revert asynchttp class when setting up internal ssl [#5159](https://github.com/jupyterhub/jupyterhub/pull/5159) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@kreuzert](https://github.com/kreuzert))
|
||||
- set HTTP status when spawn via GET params fails [#5146](https://github.com/jupyterhub/jupyterhub/pull/5146) ([@agoose77](https://github.com/agoose77), [@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- fix: use `contains_eager` instead of O(N²) `joinedload` in `init_spawners` [#5109](https://github.com/jupyterhub/jupyterhub/pull/5109) ([@agoose77](https://github.com/agoose77), [@minrk](https://github.com/minrk), [@yuvipanda](https://github.com/yuvipanda))
|
||||
- Fix hub activity log to use spawner.last_activity [#5102](https://github.com/jupyterhub/jupyterhub/pull/5102) ([@joeyutong](https://github.com/joeyutong), [@minrk](https://github.com/minrk))
|
||||
- send event if spawn_future was cancelled and spawner not pending [#5091](https://github.com/jupyterhub/jupyterhub/pull/5091) ([@kreuzert](https://github.com/kreuzert), [@manics](https://github.com/manics))
|
||||
- Fix internal ssl: do not disable hostname verification by default [#5076](https://github.com/jupyterhub/jupyterhub/pull/5076) ([@grios-stratio](https://github.com/grios-stratio), [@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Maintenance and upkeep improvements
|
||||
|
||||
- linkcheck: npmjs 403s from CI [#5162](https://github.com/jupyterhub/jupyterhub/pull/5162) ([@minrk](https://github.com/minrk))
|
||||
- unpin pytest-asyncio [#5161](https://github.com/jupyterhub/jupyterhub/pull/5161) ([@minrk](https://github.com/minrk))
|
||||
- Upgrade to font-awesome 7 [#5130](https://github.com/jupyterhub/jupyterhub/pull/5130) ([@yuvipanda](https://github.com/yuvipanda), [@minrk](https://github.com/minrk))
|
||||
- use browser.wait_for_url instead of expect(browser).to_have_url [#5120](https://github.com/jupyterhub/jupyterhub/pull/5120) ([@minrk](https://github.com/minrk))
|
||||
- jsx: add dependabot group for jest, update everything [#5119](https://github.com/jupyterhub/jupyterhub/pull/5119) ([@minrk](https://github.com/minrk))
|
||||
- Avoid double `//` in `test_proxy_service` [#5105](https://github.com/jupyterhub/jupyterhub/pull/5105) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- pytest-asyncio <1.0.0 [#5094](https://github.com/jupyterhub/jupyterhub/pull/5094) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- Switch to rbubley/mirrors-prettier, downgrade prettier to last proper release v3.6.2 [#5092](https://github.com/jupyterhub/jupyterhub/pull/5092) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- update quickstart, authenticator doc with 5.0 allow changes [#5140](https://github.com/jupyterhub/jupyterhub/pull/5140) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- update the status of access sharing UI [#5137](https://github.com/jupyterhub/jupyterhub/pull/5137) ([@akhmerov](https://github.com/akhmerov), [@krassowski](https://github.com/krassowski), [@minrk](https://github.com/minrk))
|
||||
- Add note about Windows Subsystem for Linux [#5129](https://github.com/jupyterhub/jupyterhub/pull/5129) ([@rgaiacs](https://github.com/rgaiacs), [@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- Review contributing section from documentation [#5128](https://github.com/jupyterhub/jupyterhub/pull/5128) ([@rgaiacs](https://github.com/rgaiacs), [@yuvipanda](https://github.com/yuvipanda))
|
||||
- Remove warnings about sqlite in production [#5124](https://github.com/jupyterhub/jupyterhub/pull/5124) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio))
|
||||
- allow 6 items in header drop-down [#5123](https://github.com/jupyterhub/jupyterhub/pull/5123) ([@minrk](https://github.com/minrk), [@rgaiacs](https://github.com/rgaiacs))
|
||||
- update doc links with permanent redirects [#5118](https://github.com/jupyterhub/jupyterhub/pull/5118) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics), [@rgaiacs](https://github.com/rgaiacs))
|
||||
- Collection of small improvements to contributor documentation [#5116](https://github.com/jupyterhub/jupyterhub/pull/5116) ([@rgaiacs](https://github.com/rgaiacs), [@choldgraf](https://github.com/choldgraf), [@consideRatio](https://github.com/consideRatio))
|
||||
- Docs: fix broken linkcheck [#5097](https://github.com/jupyterhub/jupyterhub/pull/5097) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio))
|
||||
- Replace Gitter with Zulip in Documentation [#5096](https://github.com/jupyterhub/jupyterhub/pull/5096) ([@rgaiacs](https://github.com/rgaiacs), [@manics](https://github.com/manics))
|
||||
- update security.md, security doc to point to GitHub vulnerability reporting [#5072](https://github.com/jupyterhub/jupyterhub/pull/5072) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@mathbunnyru](https://github.com/mathbunnyru))
|
||||
- Update docs to point to `/hub/user-redirect/` instead of `/user-redirect/` [#5071](https://github.com/jupyterhub/jupyterhub/pull/5071) ([@agoose77](https://github.com/agoose77), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add missing literal to code tags [#5070](https://github.com/jupyterhub/jupyterhub/pull/5070) ([@Paul2708](https://github.com/Paul2708), [@consideRatio](https://github.com/consideRatio))
|
||||
- Fix link in changelog [#5069](https://github.com/jupyterhub/jupyterhub/pull/5069) ([@krassowski](https://github.com/krassowski), [@minrk](https://github.com/minrk))
|
||||
- Update team compass URL [#5068](https://github.com/jupyterhub/jupyterhub/pull/5068) ([@choldgraf](https://github.com/choldgraf), [@minrk](https://github.com/minrk))
|
||||
- Fix incorrect login username in service-fastapi README.md [#5064](https://github.com/jupyterhub/jupyterhub/pull/5064) ([@chilin0525](https://github.com/chilin0525), [@manics](https://github.com/manics))
|
||||
- add forced login example [#5056](https://github.com/jupyterhub/jupyterhub/pull/5056) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- doc: spawner.delete_forever applies to users and named servers [#5052](https://github.com/jupyterhub/jupyterhub/pull/5052) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### 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=2025-04-15&to=2025-10-06&type=c))
|
||||
|
||||
@abuettner93 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aabuettner93+updated%3A2025-04-15..2025-10-06&type=Issues)) | @agoose77 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aagoose77+updated%3A2025-04-15..2025-10-06&type=Issues)) | @akhmerov ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aakhmerov+updated%3A2025-04-15..2025-10-06&type=Issues)) | @chilin0525 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Achilin0525+updated%3A2025-04-15..2025-10-06&type=Issues)) | @choldgraf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2025-04-15..2025-10-06&type=Issues)) | @clhedrick ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aclhedrick+updated%3A2025-04-15..2025-10-06&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2025-04-15..2025-10-06&type=Issues)) | @grios-stratio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agrios-stratio+updated%3A2025-04-15..2025-10-06&type=Issues)) | @joeyutong ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajoeyutong+updated%3A2025-04-15..2025-10-06&type=Issues)) | @kateryna-tarelkina-dd ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akateryna-tarelkina-dd+updated%3A2025-04-15..2025-10-06&type=Issues)) | @krassowski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akrassowski+updated%3A2025-04-15..2025-10-06&type=Issues)) | @kreuzert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akreuzert+updated%3A2025-04-15..2025-10-06&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2025-04-15..2025-10-06&type=Issues)) | @mathbunnyru ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amathbunnyru+updated%3A2025-04-15..2025-10-06&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2025-04-15..2025-10-06&type=Issues)) | @Paul2708 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3APaul2708+updated%3A2025-04-15..2025-10-06&type=Issues)) | @rgaiacs ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Argaiacs+updated%3A2025-04-15..2025-10-06&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2025-04-15..2025-10-06&type=Issues)) | @tbizouerne ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atbizouerne+updated%3A2025-04-15..2025-10-06&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2025-04-15..2025-10-06&type=Issues))
|
||||
|
||||
## 5.3
|
||||
|
||||
### 5.3.0 - 2025-04-15
|
||||
@@ -31,7 +99,7 @@ Contributors to major version bumps in JupyterHub include:
|
||||
- A new [SharedPasswordAuthenticator](#SharedPasswordAuthenticator)
|
||||
|
||||
We have also changed how we build the `jupyterhub` container images.
|
||||
Images are now built from [](https://github.com/jupyterhub/jupyterhub-container-images) instead of the JupyterHub repo.
|
||||
Images are now built from [jupyterhub-container-images](https://github.com/jupyterhub/jupyterhub-container-images) instead of the JupyterHub repo.
|
||||
The main user-facing implication of this is that image for a given JupyterHub version will be rebuilt,
|
||||
which has the following consequences:
|
||||
|
||||
@@ -1831,7 +1899,7 @@ Highlights:
|
||||
- More configuration of page templates and service display
|
||||
- Pagination of the admin page improving performance with large numbers of users
|
||||
- Improved control of user redirect
|
||||
- Support for [jupyter-server](https://jupyter-server.readthedocs.io/en/latest/)-based single-user servers, such as [Voilà](https://voila-gallery.org) and latest JupyterLab.
|
||||
- Support for [jupyter-server](https://jupyter-server.readthedocs.io/en/latest/)-based single-user servers, such as [Voilà](https://voila.readthedocs.io) and latest JupyterLab.
|
||||
- Lots more improvements to documentation, HTML pages, and customizations
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.1.0...1.2.0))
|
||||
|
@@ -16,17 +16,13 @@ Please submit pull requests to update information or to add new institutions or
|
||||
|
||||
- [BIDS - Berkeley Institute for Data Science](https://bids.berkeley.edu/)
|
||||
|
||||
- [Data 8](http://data8.org/)
|
||||
|
||||
- [Data 8](https://www.data8.org/)
|
||||
- [GitHub organization](https://github.com/data-8)
|
||||
|
||||
- [NERSC](https://www.nersc.gov/)
|
||||
|
||||
- [Press release on Jupyter and Cori](https://www.nersc.gov/news-publications/nersc-news/nersc-center-news/2016/jupyter-notebooks-will-open-up-new-possibilities-on-nerscs-cori-supercomputer/)
|
||||
- [Moving and sharing data](https://www.nersc.gov/assets/Uploads/03-MovingAndSharingData-Cholia.pdf)
|
||||
|
||||
- [Research IT](https://research-it.berkeley.edu)
|
||||
- [JupyterHub server supports campus research computation](https://research-it.berkeley.edu/blog/17/01/24/free-fully-loaded-jupyterhub-server-supports-campus-research-computation)
|
||||
- [JupyterHub server supports campus research computation](https://research-it.berkeley.edu/news/free-fully-loaded-jupyterhub-server-supports-campus-research-computation)
|
||||
|
||||
### University of California Davis
|
||||
|
||||
@@ -86,7 +82,7 @@ Within CERN, there are two noteworthy JupyterHub deployments in operation:
|
||||
|
||||
[ETH Zurich](https://ethz.ch/en.html), (Federal Institute of Technology Zurich), is a public research university in Zürich, Switzerland, with focus on science, technology, engineering, and mathematics, although its 16 departments span a variety of disciplines and subjects.
|
||||
|
||||
The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/organisation/departments/educational-development-and-technology.html) unit provides JupyterHub exclusively for teaching and learning, integrated in the learning management system [Moodle](https://ethz.ch/staffnet/en/teaching/academic-support/it-services-teaching/teaching-applications/moodle-service.html). Each course gets its individually configured JupyterHub environment deployed on a on-premise Kubernetes cluster.
|
||||
The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/organisation/departments/teaching-and-learning.html) unit provides JupyterHub exclusively for teaching and learning, integrated in the learning management system [Moodle](https://ethz.ch/staffnet/en/teaching/academic-support/it-services-teaching/teaching-applications/moodle-service.html). Each course gets its individually configured JupyterHub environment deployed on a on-premise Kubernetes cluster.
|
||||
|
||||
- [ETH JupyterHub](https://ethz.ch/staffnet/en/teaching/academic-support/it-services-teaching/teaching-applications/jupyterhub.html) for teaching and learning
|
||||
|
||||
@@ -125,16 +121,15 @@ The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/o
|
||||
### Paderborn University
|
||||
|
||||
- [Data Science (DICE) group](https://dice-research.org)
|
||||
- [nbgraderutils](https://github.com/dice-group/nbgraderutils): Use JupyterHub + nbgrader + iJava kernel for online Java exercises. Used in lecture Statistical Natural Language Processing.
|
||||
- [JavaOnlineExercises](https://github.com/dice-group/JavaOnlineExercises): Use JupyterHub + nbgrader + iJava kernel for online Java exercises. Used in lecture Statistical Natural Language Processing.
|
||||
|
||||
### Penn State University
|
||||
|
||||
- [Press release](https://news.psu.edu/story/523093/2018/05/24/new-open-source-web-apps-available-students-and-faculty): "New open-source web apps available for students and faculty"
|
||||
- [Press release](https://www.psu.edu/news/academics/story/new-open-source-web-apps-available-students-and-faculty): "New open-source web apps available for students and faculty"
|
||||
|
||||
### University of California San Diego
|
||||
|
||||
- San Diego Supercomputer Center - Andrea Zonca
|
||||
|
||||
- [Deploy JupyterHub on a Supercomputer with SSH](https://zonca.github.io/2017/05/jupyterhub-hpc-batchspawner-ssh.html)
|
||||
- [Run Jupyterhub on a Supercomputer](https://zonca.github.io/2015/04/jupyterhub-hpc.html)
|
||||
- [Deploy JupyterHub on a VM for a Workshop](https://zonca.github.io/2016/04/jupyterhub-sdsc-cloud.html)
|
||||
@@ -154,7 +149,7 @@ The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/o
|
||||
|
||||
### Elucidata
|
||||
|
||||
- What's new in Jupyter Notebooks @[Elucidata](https://elucidata.io/):
|
||||
- What's new in Jupyter Notebooks @[Elucidata](https://www.elucidata.io/):
|
||||
- [Using Jupyter Notebooks with Jupyterhub on GCP, managed by GKE](https://medium.com/elucidata/why-you-should-be-using-a-jupyter-notebook-8385a4ccd93d)
|
||||
|
||||
## Service Providers
|
||||
@@ -174,7 +169,7 @@ The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/o
|
||||
|
||||
### Microsoft Azure
|
||||
|
||||
- [Azure Data Science Virtual Machine release notes](https://docs.microsoft.com/en-us/azure/machine-learning/machine-learning-data-science-linux-dsvm-intro)
|
||||
- [Azure Data Science Virtual Machine release notes](https://learn.microsoft.com/en-us/azure/machine-learning/machine-learning-data-science-linux-dsvm-intro)
|
||||
|
||||
### Rackspace Carina
|
||||
|
||||
@@ -202,5 +197,5 @@ The [Educational Development and Technology](https://ethz.ch/en/the-eth-zurich/o
|
||||
- https://www.walkingrandomly.com/?p=5734
|
||||
- https://wrdrd.com/docs/consulting/education-technology
|
||||
- https://bitbucket.org/jackhale/fenics-jupyter
|
||||
- [LinuxCluster blog](https://linuxcluster.wordpress.com/category/application/jupyterhub/)
|
||||
- [LinuxCluster blog](https://thelinuxcluster.com/category/application/jupyterhub/)
|
||||
- [Spark Cluster on OpenStack with Multi-User Jupyter Notebook](https://arnesund.com/2015/09/21/spark-cluster-on-openstack-with-multi-user-jupyter-notebook/)
|
||||
|
@@ -563,7 +563,7 @@ and an example of its configuration is found [here](https://github.com/jupyter/n
|
||||
nbviewer can also be run as a Hub-Managed Service as described [nbviewer README][nbviewer example]
|
||||
section on securing the notebook viewer.
|
||||
|
||||
[requests]: https://docs.python-requests.org/en/master/
|
||||
[requests]: https://requests.readthedocs.io
|
||||
[services_auth]: ../api/services.auth.html
|
||||
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer
|
||||
[fastapi example]: https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-fastapi
|
||||
|
@@ -201,13 +201,13 @@ To revoke sharing permissions from the perspective of the user or group being sh
|
||||
you need the permissions `users:shares` or `groups:shares` with the appropriate _user_ or _group_ filter.
|
||||
This allows users to 'leave' shared servers, without needing permission to manage the server's sharing permissions.
|
||||
|
||||
```
|
||||
```{parsed-literal}
|
||||
[DELETE /api/users/:username/shared/:ownername/:servername](rest-api-delete-user-shared-server)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
```{parsed-literal}
|
||||
[DELETE /api/groups/:groupname/shared/:ownername/:servername](rest-api-delete-group-shared-server)
|
||||
```
|
||||
|
||||
|
@@ -467,7 +467,7 @@ spawner, does not support limits and guarantees. One of the spawners
|
||||
that supports limits and guarantees is the
|
||||
[`systemdspawner`](https://github.com/jupyterhub/systemdspawner).
|
||||
|
||||
### Memory Limits & Guarantees
|
||||
### Memory Limits and Guarantees
|
||||
|
||||
`c.Spawner.mem_limit`: A **limit** specifies the _maximum amount of memory_
|
||||
that may be allocated, though there is no promise that the maximum amount will
|
||||
@@ -487,7 +487,7 @@ available for the single-user notebook server to use. The environment variable
|
||||
limits and providing these guarantees.** If these values are set to `None`, no
|
||||
limits or guarantees are provided, and no environment values are set.
|
||||
|
||||
### CPU Limits & Guarantees
|
||||
### CPU Limits and Guarantees
|
||||
|
||||
`c.Spawner.cpu_limit`: In supported spawners, you can set
|
||||
`c.Spawner.cpu_limit` to limit the total number of cpu-cores that a
|
||||
|
@@ -169,14 +169,14 @@ _Version changed: 1.0_
|
||||
JupyterHub version 0.9 failed these API requests with status `404`,
|
||||
but version 1.0 uses 503.
|
||||
|
||||
## `/user-redirect/...`
|
||||
## `/hub/user-redirect/...`
|
||||
|
||||
The `/user-redirect/...` URL is for sharing a URL that will redirect a user
|
||||
The `/hub/user-redirect/...` URL is for sharing a URL that will redirect a user
|
||||
to a path on their own default server.
|
||||
This is useful when different users have the same file at the same URL on their servers,
|
||||
and you want a single link to give to any user that will open that file on their server.
|
||||
|
||||
e.g. a link to `/user-redirect/notebooks/Index.ipynb`
|
||||
e.g. a link to `/hub/user-redirect/notebooks/Index.ipynb`
|
||||
will send user `hortense` to `/user/hortense/notebooks/Index.ipynb`
|
||||
|
||||
**DO NOT** share links to your own server with other users.
|
||||
|
@@ -2,9 +2,15 @@
|
||||
|
||||
# Authentication and User Basics
|
||||
|
||||
The default Authenticator uses [PAM][] (Pluggable Authentication Module) to authenticate system users with
|
||||
their usernames and passwords. With the default Authenticator, any user
|
||||
with an account and password on the system will be allowed to login.
|
||||
The default Authenticator uses [PAM][] (Pluggable Authentication Module) to authenticate users already defined on the system with their usernames and passwords.
|
||||
With the default Authenticator,
|
||||
any user with an account and password on the system will be able to login.
|
||||
But that does not mean they will be **allowed** to access JupyterHub.
|
||||
|
||||
:::{important}
|
||||
Only _explicitly allowed_ users can login to JupyterHub
|
||||
(a user who can login but is not allowed will see a permission error after successful login).
|
||||
:::
|
||||
|
||||
## Deciding who is allowed
|
||||
|
||||
|
@@ -46,7 +46,7 @@ If you want to run docker on a computer that has a public IP then you should
|
||||
(as in MUST) **secure it with ssl** by adding ssl options to your docker
|
||||
configuration or using an ssl enabled proxy.
|
||||
|
||||
[Mounting volumes](https://docs.docker.com/engine/admin/volumes/volumes/)
|
||||
[Mounting volumes](https://docs.docker.com/engine/storage/volumes/)
|
||||
enables you to persist and store the data generated by the docker container, even when you stop the container.
|
||||
The persistent data can be stored on the host system, outside the container.
|
||||
|
||||
|
@@ -11,7 +11,6 @@ Before installing JupyterHub, you will need:
|
||||
installing Python packages is helpful.
|
||||
- [Node.js {{node_min}}](https://www.npmjs.com/) or greater, along with npm. [Install Node.js/npm](https://docs.npmjs.com/getting-started/installing-node),
|
||||
using your operating system's package manager.
|
||||
|
||||
- If you are using **`conda`**, the nodejs and npm dependencies will be installed for
|
||||
you by conda.
|
||||
|
||||
@@ -72,6 +71,35 @@ jupyterhub -h
|
||||
configurable-http-proxy -h
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
At this point, we could start jupyterhub, but nobody would be able to use it!
|
||||
Only users who are explicitly **allowed** can use JupyterHub.
|
||||
To allow users, we need to create a configuration file.
|
||||
JupyterHub uses a configuration file called `jupyterhub_config.py`,
|
||||
which is a regular Python script with one function `get_config()` pre-defined, returning the "config object".
|
||||
Assigning attributes to this object is how we configure JupyterHub.
|
||||
|
||||
At this point, we have two choices:
|
||||
|
||||
1. allow any user who can successfully login with our Authenticator (often a good choice for local machines with PAM)
|
||||
2. allow one or more users by name.
|
||||
|
||||
We'll start with the first one.
|
||||
Create the file `jupyerhub_config.py` with the content:
|
||||
|
||||
```python
|
||||
c = get_config() # noqa
|
||||
c.Authenticator.allow_all = True
|
||||
# alternative: c.Authenticator.allowed_users = {"yourusername"}
|
||||
```
|
||||
|
||||
This configuration means that anyone who can login with PAM (any existing user on the system) should have access to JupyterHub.
|
||||
|
||||
:::{seealso}
|
||||
[](authenticators)
|
||||
:::
|
||||
|
||||
## Start the Hub server
|
||||
|
||||
To start the Hub server, run the command:
|
||||
|
@@ -51,25 +51,31 @@ Any shared permissions previously granted by a user will remain and must be revo
|
||||
if desired.
|
||||
:::
|
||||
|
||||
### Grant servers permission to share themselves (optional, admin)
|
||||
### Grant servers permission to share themselves (admin)
|
||||
|
||||
The most natural place to want to grant access to a server is when viewing that server.
|
||||
By default, the tokens used when talking to a server have extremely limited permissions.
|
||||
You can grant sharing permissions to servers themselves in one of two ways.
|
||||
When you want users to be able to share access while viewing a server, grant the appropriate
|
||||
sharing scopes so the server or the browser token can manage sharing. By default, tokens used
|
||||
to talk to a server have limited permissions.
|
||||
|
||||
The first is to grant sharing permission to the tokens used by browser requests.
|
||||
This is what you would do if you had a JupyterLab extension that presented UI for managing shares
|
||||
(this should exist! We haven't made it yet).
|
||||
To grant these tokens sharing permissions:
|
||||
Granting browser-originating tokens the sharing scopes is the recommended approach when using
|
||||
JupyterLab with the `jupyter-collaboration` extension, which provides a UI for managing shares.
|
||||
The minimal permissions required to allow browser tokens to request sharing-related scopes are:
|
||||
|
||||
```python
|
||||
c.Spawner.oauth_client_allowed_scopes = ["access:servers!server", "shares!server"]
|
||||
```
|
||||
|
||||
JupyterHub's `user-sharing` example does it this way.
|
||||
The `jupyter-collaboration` UI requires additional Hub scopes to share their server with specific users on the Hub:
|
||||
|
||||
```python
|
||||
c.Spawner.oauth_client_allowed_scopes = [
|
||||
"read:users:name", "shares!user", "list:users", "servers!user"
|
||||
]
|
||||
```
|
||||
|
||||
The nice thing about this approach is that only users who already have those permissions will get a token which can take these actions.
|
||||
The downside (in terms of convenience) is that the browser token is only accessible to the javascript (e.g. JupyterLab) and/or jupyter-server request handlers,
|
||||
but not notebooks or terminals.
|
||||
The downside is that the browser token is only accessible to the javascript (e.g. JupyterLab) and/or jupyter-server request handlers, but not notebooks or terminals.
|
||||
|
||||
The second way, which is less secure, but perhaps more convenient for demonstration purposes,
|
||||
is to grant the _server itself_ permission to grant access to itself.
|
||||
|
51
examples/forced-login/README.md
Normal file
51
examples/forced-login/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Forced login example
|
||||
|
||||
Example for forcing user login via URL without disabling token-in-url protection.
|
||||
|
||||
An external application issues tokens associated with usernames.
|
||||
A JupyterHub Authenticator only allows login via these tokens in a URL parameter (`/hub/login?login_token=....`),
|
||||
which are then exchanged for a username, which is used to login the user.
|
||||
|
||||
Each token can be used for login only once, and must be used within 30 seconds of issue.
|
||||
|
||||
To run:
|
||||
|
||||
in one shell:
|
||||
|
||||
```
|
||||
python3 external_app.py
|
||||
```
|
||||
|
||||
in another:
|
||||
|
||||
```
|
||||
jupyterhub
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:9000
|
||||
|
||||
Sometimes, JupyterHub is integrated into an existing application,
|
||||
which has already handled login, etc.
|
||||
It is often preferable in these applications to be able to link users to their running JupyterHub server without _prompting_ the user for login to the Hub when the Hub should really be an implementation detail.
|
||||
|
||||
One way to do this has been to use "API only mode", issue tokens for users, and redirect users to a URL like `/users/name/?token=abc123`.
|
||||
This is [disabled by default]() in JupyterHub 5, because it presents a vulnerability for users to craft links that let _other_ users login as them, which can lead to inter-user attacks.
|
||||
|
||||
But that leaves the question: how do I as an _application developer_ generate a link that can login a user?
|
||||
|
||||
_Ideally_, the best way to set this up is with the external service as an OAuth provider,
|
||||
though in some cases it works best to use proxy-based authentication like Shibboleth / [REMOTE_USER]().
|
||||
|
||||
If your service is an OAuth provider, sharing links to `/hub/user-redirect/lab/tree/path/to/notebook...` should work just fine.
|
||||
JupyterHub will:
|
||||
|
||||
1. authenticate the user
|
||||
2. redirect to your identity provider via oauth (you can set `Authenticator.auto_login = True` if you want to skip prompting the user)
|
||||
3. complete oauth
|
||||
4. start their single-user server if it's not running (show the launch progress page while it's waiting)
|
||||
5. redirect to their server once it's up
|
||||
6. oauth (again), this time between the single-user server and the Hub
|
||||
|
||||
If your application chooses to launch the server and wait for it to be ready before redirecting
|
||||
|
||||
[API only mode]() is sometimes useful
|
100
examples/forced-login/external_app.py
Normal file
100
examples/forced-login/external_app.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""An external app for laucnhing JupyuterHub with specified usernames
|
||||
|
||||
This one serves a form with a single username input field
|
||||
|
||||
After entering the username, generate a token and redirect to hub login with that token,
|
||||
which is then exchanged for a username.
|
||||
|
||||
Users cannot login to JupyterHub directly, only via this app.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Body, FastAPI, Form, status
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from yarl import URL
|
||||
|
||||
from jupyterhub.utils import url_path_join
|
||||
|
||||
app_dir = Path(__file__).parent.resolve()
|
||||
index_html = app_dir / "index.html"
|
||||
app = FastAPI()
|
||||
|
||||
log = logging.getLogger("uvicorn.error")
|
||||
|
||||
_tokens_to_username = {}
|
||||
|
||||
jupyterhub_url = URL(os.environ.get("JUPYTERHUB_URL", "http://127.0.0.1:8000/"))
|
||||
|
||||
# how many seconds do they have to complete the exchange before the token expires?
|
||||
token_lifetime = 30
|
||||
|
||||
|
||||
def _hash(token):
|
||||
"""Hash a token for storage"""
|
||||
return hashlib.sha256(token.encode("utf8", "replace")).hexdigest()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def get():
|
||||
with index_html.open() as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
|
||||
@app.post("/")
|
||||
async def launch(username: Annotated[str, Form()], path: Annotated[str, Form()]):
|
||||
"""Begin login
|
||||
|
||||
1. issue token for login
|
||||
2. associate token with username
|
||||
3. redirect to /hub/login?login_token=...
|
||||
"""
|
||||
token = secrets.token_urlsafe(32)
|
||||
hashed_token = _hash(token)
|
||||
log.info(f"Creating token for {username}, redirecting to {path}")
|
||||
_tokens_to_username[hashed_token] = (username, time.monotonic() + token_lifetime)
|
||||
login_url = (jupyterhub_url / "hub/login").extend_query(
|
||||
login_token=token, next=url_path_join("/hub/user-redirect", path)
|
||||
)
|
||||
log.info(login_url)
|
||||
|
||||
return RedirectResponse(login_url, status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@app.post("/login", response_class=JSONResponse)
|
||||
async def login(token: Annotated[str, Body(embed=True)]):
|
||||
"""
|
||||
Callback to exchange a token for a username
|
||||
|
||||
token is consumed, can only be used once
|
||||
"""
|
||||
now = time.monotonic()
|
||||
hashed_token = _hash(token)
|
||||
if hashed_token not in _tokens_to_username:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND, content={"message": "invalid token"}
|
||||
)
|
||||
username, expires_at = _tokens_to_username.pop(hashed_token)
|
||||
if expires_at < now:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": "token expired"},
|
||||
)
|
||||
return {"name": username}
|
||||
|
||||
|
||||
def main():
|
||||
"""Launches the application on port 5000 with uvicorn"""
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, port=9000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
22
examples/forced-login/index.html
Normal file
22
examples/forced-login/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>External Service Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login to JupyterHub</h1>
|
||||
<form action="" method="POST">
|
||||
<label for="username">
|
||||
Username:
|
||||
<input type="text" name="username" autocomplete="off" />
|
||||
</label>
|
||||
<br />
|
||||
<label for="path">
|
||||
Redirect path:
|
||||
<input type="text" name="path" autocomplete="off" value="/lab" />
|
||||
</label>
|
||||
<br />
|
||||
<button>Login</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
65
examples/forced-login/jupyterhub_config.py
Normal file
65
examples/forced-login/jupyterhub_config.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
|
||||
from tornado import web
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPClientError
|
||||
from traitlets import Unicode
|
||||
|
||||
from jupyterhub.auth import Authenticator
|
||||
from jupyterhub.utils import url_path_join
|
||||
|
||||
|
||||
class ForcedLoginAuthenticator(Authenticator):
|
||||
"""Authenticator to force login with a token provided by an external service
|
||||
|
||||
The external service issues tokens, which are exchanged for a username.
|
||||
Visiting `/hub/login?login_token=...` logs in a user
|
||||
Each token can be used only once.
|
||||
"""
|
||||
|
||||
auto_login = True # begin login without prompt (token is in url)
|
||||
allow_all = True # external login app controls this
|
||||
token_provider_url = Unicode(
|
||||
config=True, help="""The URL of the token/username provider"""
|
||||
)
|
||||
|
||||
async def authenticate(self, handler, data):
|
||||
token = handler.get_argument("login_token", None)
|
||||
if not token:
|
||||
raise web.HTTPError(
|
||||
400, f"Login with external provider at {self.token_provider_url}"
|
||||
)
|
||||
client = AsyncHTTPClient()
|
||||
try:
|
||||
response = await client.fetch(
|
||||
url_path_join(self.token_provider_url, "/login"),
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({"token": token}),
|
||||
)
|
||||
except HTTPClientError as e:
|
||||
self.log.info(
|
||||
"Error exchanging token for username: %s",
|
||||
e.response.body.decode("utf8", "replace"),
|
||||
)
|
||||
if e.code == 404:
|
||||
raise web.HTTPError(
|
||||
403,
|
||||
f"Invalid token. Login with external provider at {self.token_provider_url}",
|
||||
)
|
||||
else:
|
||||
raise
|
||||
# pass through the response
|
||||
return json.loads(response.body.decode())
|
||||
|
||||
|
||||
c = get_config() # noqa
|
||||
|
||||
# use our Authenticator
|
||||
c.JupyterHub.authenticator_class = ForcedLoginAuthenticator
|
||||
# tell it where the external launch app is
|
||||
c.ForcedLoginAuthenticator.token_provider_url = "http://127.0.0.1:9000/"
|
||||
|
||||
|
||||
# local testing config (fake spawner, localhost only)
|
||||
c.JupyterHub.ip = "127.0.0.1"
|
||||
c.JupyterHub.spawner_class = "simple"
|
3
examples/forced-login/requirements.txt
Normal file
3
examples/forced-login/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
jupyterhub
|
||||
yarl
|
@@ -60,7 +60,7 @@ sudo docker build . -t service-fastapi
|
||||
sudo docker run -it -p 8000:8000 service-fastapi
|
||||
```
|
||||
|
||||
2. Visit http://127.0.0.1:8000/services/fastapi/docs. When going through the OAuth flow or getting a token from the control panel, you can log in with `testuser` / `passwd`.
|
||||
2. Visit http://127.0.0.1:8000/services/fastapi/docs. When going through the OAuth flow or getting a token from the control panel, you can log in with 'test-user' and any password.
|
||||
|
||||
# PUBLIC_HOST
|
||||
|
||||
|
8019
jsx/package-lock.json
generated
8019
jsx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,45 +35,45 @@
|
||||
"testEnvironment": "jsdom"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.5",
|
||||
"bootstrap": "^5.3.8",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-dom": "^19.1.0",
|
||||
"react": "^19.1.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"redux": "^5.0.1",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.10",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@webpack-cli/serve": "^3.0.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-jest": "^30.2.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"prettier": "^3.5.3",
|
||||
"jest": "^30.1.2",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack": "^5.102.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
@@ -139,7 +139,7 @@ test("Interacting with PaginationFooter causes page refresh", async () => {
|
||||
render(groupsJsx(updateGroupsSpy));
|
||||
});
|
||||
|
||||
expect(updateGroupsSpy).toBeCalledWith(0, 2);
|
||||
expect(updateGroupsSpy).toHaveBeenCalledWith(0, 2);
|
||||
|
||||
var lastState =
|
||||
mockReducers.mock.results[mockReducers.mock.results.length - 1].value;
|
||||
@@ -153,5 +153,5 @@ test("Interacting with PaginationFooter causes page refresh", async () => {
|
||||
});
|
||||
expect(searchParams.get("offset")).toEqual("2");
|
||||
// FIXME: useSelector mocks prevent updateGroups from being called
|
||||
// expect(updateGroupsSpy).toBeCalledWith(2, 2);
|
||||
// expect(updateGroupsSpy).toHaveBeenCalledWith(2, 2);
|
||||
});
|
||||
|
@@ -591,14 +591,14 @@ test("Search for user calls updateUsers with name filter", async () => {
|
||||
expect(searchParams.get("offset")).toEqual(null);
|
||||
// FIXME: useSelector mocks prevent updateUsers from being called
|
||||
// expect(mockUpdateUsers.mock.calls).toHaveLength(2);
|
||||
// expect(mockUpdateUsers).toBeCalledWith(0, 100, "a");
|
||||
// expect(mockUpdateUsers).toHaveBeenCalledWith(0, 100, "a");
|
||||
await user.type(search, "b");
|
||||
expect(search.value).toEqual("ab");
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(searchParams.get("name_filter")).toEqual("ab");
|
||||
// expect(mockUpdateUsers).toBeCalledWith(0, 100, "ab");
|
||||
// expect(mockUpdateUsers).toHaveBeenCalledWith(0, 100, "ab");
|
||||
});
|
||||
|
||||
test("Interacting with PaginationFooter requests page update", async () => {
|
||||
@@ -606,7 +606,7 @@ test("Interacting with PaginationFooter requests page update", async () => {
|
||||
render(serverDashboardJsx());
|
||||
});
|
||||
|
||||
expect(mockUpdateUsers).toBeCalledWith(defaultUpdateUsersParams);
|
||||
expect(mockUpdateUsers).toHaveBeenCalledWith(defaultUpdateUsersParams);
|
||||
|
||||
var n = 3;
|
||||
expect(searchParams.get("offset")).toEqual(null);
|
||||
@@ -619,7 +619,7 @@ test("Interacting with PaginationFooter requests page update", async () => {
|
||||
});
|
||||
expect(searchParams.get("offset")).toEqual("2");
|
||||
// FIXME: useSelector mocks prevent updateUsers from being called
|
||||
// expect(mockUpdateUsers).toBeCalledWith({
|
||||
// expect(mockUpdateUsers).toHaveBeenCalledWith({
|
||||
// ...defaultUpdateUsersParams,
|
||||
// offset: 2,
|
||||
// });
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (5, 3, 0, "", "")
|
||||
version_info = (5, 4, 0, "", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -24,6 +24,7 @@ from ..roles import assign_default_roles
|
||||
from ..scopes import needs_scope
|
||||
from ..user import User
|
||||
from ..utils import (
|
||||
format_exception,
|
||||
isoformat,
|
||||
iterate_until,
|
||||
maybe_future,
|
||||
@@ -865,15 +866,14 @@ class SpawnProgressAPIHandler(APIHandler):
|
||||
failed_event['message'] = "Spawn cancelled"
|
||||
elif f and f.done() and f.exception():
|
||||
exc = f.exception()
|
||||
message = getattr(exc, "jupyterhub_message", str(exc))
|
||||
message, html_message = format_exception(exc)
|
||||
failed_event['message'] = f"Spawn failed: {message}"
|
||||
html_message = getattr(exc, "jupyterhub_html_message", "")
|
||||
if html_message:
|
||||
failed_event['html_message'] = html_message
|
||||
await self.send_event(failed_event)
|
||||
return
|
||||
else:
|
||||
raise web.HTTPError(400, "%s is not starting...", spawner._log_name)
|
||||
await self.send_event(failed_event)
|
||||
return
|
||||
|
||||
# retrieve progress events from the Spawner
|
||||
async with aclosing(
|
||||
@@ -906,9 +906,8 @@ class SpawnProgressAPIHandler(APIHandler):
|
||||
failed_event['message'] = "Spawn cancelled"
|
||||
elif f and f.done() and f.exception():
|
||||
exc = f.exception()
|
||||
message = getattr(exc, "jupyterhub_message", str(exc))
|
||||
message, html_message = format_exception(exc)
|
||||
failed_event['message'] = f"Spawn failed: {message}"
|
||||
html_message = getattr(exc, "jupyterhub_html_message", "")
|
||||
if html_message:
|
||||
failed_event['html_message'] = html_message
|
||||
else:
|
||||
@@ -1034,7 +1033,7 @@ class ActivityAPIHandler(APIHandler):
|
||||
user.name,
|
||||
server_name,
|
||||
isoformat(last_activity),
|
||||
isoformat(user.last_activity),
|
||||
isoformat(spawner.last_activity),
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
|
@@ -32,7 +32,7 @@ from dateutil.parser import parse as parse_date
|
||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
|
||||
from jupyter_events.logger import EventLogger
|
||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import contains_eager, selectinload
|
||||
from tornado import gen, web
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
@@ -1984,12 +1984,16 @@ class JupyterHub(Application):
|
||||
|
||||
# Configure the AsyncHTTPClient. This will affect anything using
|
||||
# AsyncHTTPClient.
|
||||
ssl_context = make_ssl_context(
|
||||
self.internal_ssl_key,
|
||||
self.internal_ssl_cert,
|
||||
cafile=self.internal_ssl_ca,
|
||||
# can't use ssl_options in case of pycurl
|
||||
AsyncHTTPClient.configure(
|
||||
AsyncHTTPClient.configured_class(),
|
||||
defaults=dict(
|
||||
ca_certs=self.internal_ssl_ca,
|
||||
client_key=self.internal_ssl_key,
|
||||
client_cert=self.internal_ssl_cert,
|
||||
validate_cert=True,
|
||||
),
|
||||
)
|
||||
AsyncHTTPClient.configure(None, defaults={"ssl_options": ssl_context})
|
||||
|
||||
def init_db(self):
|
||||
"""Create the database connection"""
|
||||
@@ -3097,9 +3101,10 @@ class JupyterHub(Application):
|
||||
.filter(orm.Spawner.server != None)
|
||||
# pre-load relationships to avoid O(N active servers) queries
|
||||
.options(
|
||||
joinedload(orm.User._orm_spawners),
|
||||
joinedload(orm.Spawner.server),
|
||||
contains_eager(orm.User._orm_spawners),
|
||||
selectinload(orm.Spawner.server),
|
||||
)
|
||||
.populate_existing()
|
||||
):
|
||||
# instantiate Spawner wrapper and check if it's still alive
|
||||
# spawner should be running
|
||||
@@ -3886,6 +3891,10 @@ class JupyterHub(Application):
|
||||
tasks = [t for t in asyncio.all_tasks()]
|
||||
for t in tasks:
|
||||
self.log.debug("Task status: %s", t)
|
||||
self._stop_event_loop()
|
||||
|
||||
def _stop_event_loop(self):
|
||||
"""In a method to allow tests to not do this"""
|
||||
asyncio.get_event_loop().stop()
|
||||
|
||||
def stop(self):
|
||||
|
@@ -86,7 +86,7 @@ class Authenticator(LoggingConfigurable):
|
||||
auth info will never be considered stale.
|
||||
|
||||
Set `auth_refresh_age = 0` to disable time-based calls to `refresh_user`.
|
||||
You can still use :attr:`refresh_pre_spawn` if `auth_refresh_age` is disabled.
|
||||
You can still use :attr:`refresh_pre_spawn` or :attr:`refresh_pre_stop` if `auth_refresh_age` is disabled.
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -106,6 +106,25 @@ class Authenticator(LoggingConfigurable):
|
||||
""",
|
||||
)
|
||||
|
||||
refresh_pre_stop = Bool(
|
||||
False,
|
||||
config=True,
|
||||
help="""Force refresh of auth prior to stop.
|
||||
|
||||
This forces :meth:`.refresh_user` to be called prior to stopping
|
||||
a server, to ensure that auth state is up-to-date.
|
||||
|
||||
This can be important when e.g. auth tokens stored in auth_state may have expired,
|
||||
but are a required part of the Spawner's shutdown steps.
|
||||
|
||||
If refresh_user cannot refresh the user auth data,
|
||||
stop will fail until the user logs in again.
|
||||
If an admin initiates the stop, it will proceed regardless.
|
||||
|
||||
.. versionadded:: 5.4
|
||||
""",
|
||||
)
|
||||
|
||||
admin_users = Set(
|
||||
help="""
|
||||
Set of users that will be granted admin rights on this JupyterHub.
|
||||
|
@@ -1326,6 +1326,22 @@ class BaseHandler(RequestHandler):
|
||||
spawner = user.spawners[server_name]
|
||||
if spawner.pending:
|
||||
raise RuntimeError(f"{spawner._log_name} pending {spawner.pending}")
|
||||
|
||||
if self.authenticator.refresh_pre_stop:
|
||||
auth_user = await self.refresh_auth(user, force=True)
|
||||
if auth_user is None:
|
||||
if (
|
||||
self.current_user.kind == "user"
|
||||
and self.current_user.name == user.name
|
||||
):
|
||||
raise web.HTTPError(
|
||||
403, "auth has expired for %s, login again", user.name
|
||||
)
|
||||
else:
|
||||
self.log.warning(
|
||||
"User %s may have stale auth info. Stopping anyway.", user.name
|
||||
)
|
||||
|
||||
# set user._stop_pending before doing anything async
|
||||
# to avoid races
|
||||
spawner._stop_pending = True
|
||||
|
@@ -15,7 +15,13 @@ from tornado.httputil import url_concat
|
||||
from .. import __version__, orm
|
||||
from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus
|
||||
from ..scopes import describe_raw_scopes, needs_scope
|
||||
from ..utils import maybe_future, url_escape_path, url_path_join, utcnow
|
||||
from ..utils import (
|
||||
format_exception,
|
||||
maybe_future,
|
||||
url_escape_path,
|
||||
url_path_join,
|
||||
utcnow,
|
||||
)
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
@@ -92,7 +98,9 @@ class SpawnHandler(BaseHandler):
|
||||
|
||||
default_url = None
|
||||
|
||||
async def _render_form(self, for_user, spawner_options_form, message=''):
|
||||
async def _render_form(
|
||||
self, for_user, spawner_options_form, message='', html_message=''
|
||||
):
|
||||
auth_state = await for_user.get_auth_state()
|
||||
return await self.render_template(
|
||||
'spawn.html',
|
||||
@@ -100,6 +108,7 @@ class SpawnHandler(BaseHandler):
|
||||
auth_state=auth_state,
|
||||
spawner_options_form=spawner_options_form,
|
||||
error_message=message,
|
||||
html_error_message=html_message,
|
||||
url=url_concat(
|
||||
self.request.uri, {"_xsrf": self.xsrf_token.decode('ascii')}
|
||||
),
|
||||
@@ -177,7 +186,6 @@ class SpawnHandler(BaseHandler):
|
||||
await spawner.run_auth_state_hook(auth_state)
|
||||
|
||||
# Try to start server directly when query arguments are passed.
|
||||
error_message = ''
|
||||
query_options = {}
|
||||
for key, byte_list in self.request.query_arguments.items():
|
||||
query_options[key] = [bs.decode('utf8') for bs in byte_list]
|
||||
@@ -185,6 +193,8 @@ class SpawnHandler(BaseHandler):
|
||||
# 'next' is reserved argument for redirect after spawn
|
||||
query_options.pop('next', None)
|
||||
|
||||
spawn_exc = None
|
||||
|
||||
if len(query_options) > 0:
|
||||
try:
|
||||
self.log.debug(
|
||||
@@ -200,16 +210,31 @@ class SpawnHandler(BaseHandler):
|
||||
"Failed to spawn single-user server with query arguments",
|
||||
exc_info=True,
|
||||
)
|
||||
error_message = str(e)
|
||||
spawn_exc = e
|
||||
# fallback to behavior without failing query arguments
|
||||
|
||||
spawner_options_form = await spawner.get_options_form()
|
||||
if spawner_options_form:
|
||||
self.log.debug("Serving options form for %s", spawner._log_name)
|
||||
|
||||
# Explicitly catch HTTPError and report them to the client
|
||||
# This may need scoping to particular error codes.
|
||||
if isinstance(spawn_exc, web.HTTPError):
|
||||
self.set_status(spawn_exc.status_code)
|
||||
|
||||
for name, value in spawn_exc.headers.items():
|
||||
self.set_header(name, value)
|
||||
|
||||
if spawn_exc:
|
||||
error_message, error_html_message = format_exception(spawn_exc)
|
||||
else:
|
||||
error_message = error_html_message = None
|
||||
|
||||
form = await self._render_form(
|
||||
for_user=user,
|
||||
spawner_options_form=spawner_options_form,
|
||||
message=error_message,
|
||||
html_message=error_html_message,
|
||||
)
|
||||
self.finish(form)
|
||||
else:
|
||||
@@ -265,9 +290,23 @@ class SpawnHandler(BaseHandler):
|
||||
self.log.error(
|
||||
"Failed to spawn single-user server with form", exc_info=True
|
||||
)
|
||||
|
||||
# Explicitly catch HTTPError and report them to the client
|
||||
# This may need scoping to particular error codes.
|
||||
if isinstance(e, web.HTTPError):
|
||||
self.set_status(e.status_code)
|
||||
|
||||
for name, value in e.headers.items():
|
||||
self.set_header(name, value)
|
||||
|
||||
error_message, error_html_message = format_exception(e)
|
||||
|
||||
spawner_options_form = await user.spawner.get_options_form()
|
||||
form = await self._render_form(
|
||||
for_user=user, spawner_options_form=spawner_options_form, message=str(e)
|
||||
for_user=user,
|
||||
spawner_options_form=spawner_options_form,
|
||||
message=error_message,
|
||||
html_message=error_html_message,
|
||||
)
|
||||
self.finish(form)
|
||||
return
|
||||
@@ -379,6 +418,8 @@ class SpawnPendingHandler(BaseHandler):
|
||||
if isinstance(exc, web.HTTPError):
|
||||
status_code = exc.status_code
|
||||
self.set_status(status_code)
|
||||
|
||||
message, html_message = format_exception(exc, only_jupyterhub=True)
|
||||
html = await self.render_template(
|
||||
"not_running.html",
|
||||
user=user,
|
||||
@@ -386,8 +427,8 @@ class SpawnPendingHandler(BaseHandler):
|
||||
server_name=server_name,
|
||||
spawn_url=spawn_url,
|
||||
failed=True,
|
||||
failed_html_message=getattr(exc, 'jupyterhub_html_message', ''),
|
||||
failed_message=getattr(exc, 'jupyterhub_message', ''),
|
||||
failed_html_message=html_message,
|
||||
failed_message=message,
|
||||
exception=exc,
|
||||
)
|
||||
self.finish(html)
|
||||
|
@@ -47,7 +47,6 @@ from jupyterhub.utils import (
|
||||
_bool_env,
|
||||
exponential_backoff,
|
||||
isoformat,
|
||||
make_ssl_context,
|
||||
url_path_join,
|
||||
)
|
||||
|
||||
@@ -325,12 +324,16 @@ class JupyterHubSingleUser(ExtensionApp):
|
||||
|
||||
@default('hub_http_client')
|
||||
def _default_client(self):
|
||||
ssl_context = make_ssl_context(
|
||||
self.hub_auth.keyfile,
|
||||
self.hub_auth.certfile,
|
||||
cafile=self.hub_auth.client_ca,
|
||||
# can't use ssl_options in case of pycurl
|
||||
AsyncHTTPClient.configure(
|
||||
AsyncHTTPClient.configured_class(),
|
||||
defaults=dict(
|
||||
ca_certs=self.hub_auth.client_ca,
|
||||
client_key=self.hub_auth.keyfile,
|
||||
client_cert=self.hub_auth.certfile,
|
||||
validate_cert=True,
|
||||
),
|
||||
)
|
||||
AsyncHTTPClient.configure(None, defaults={"ssl_options": ssl_context})
|
||||
return AsyncHTTPClient()
|
||||
|
||||
async def check_hub_version(self):
|
||||
|
@@ -49,7 +49,6 @@ from ..utils import (
|
||||
_bool_env,
|
||||
exponential_backoff,
|
||||
isoformat,
|
||||
make_ssl_context,
|
||||
url_path_join,
|
||||
)
|
||||
from ._decorator import allow_unauthenticated
|
||||
@@ -403,10 +402,16 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
|
||||
@default('hub_http_client')
|
||||
def _default_client(self):
|
||||
ssl_context = make_ssl_context(
|
||||
self.keyfile, self.certfile, cafile=self.client_ca
|
||||
# can't use ssl_options in case of pycurl
|
||||
AsyncHTTPClient.configure(
|
||||
AsyncHTTPClient.configured_class(),
|
||||
defaults=dict(
|
||||
ca_certs=self.client_ca,
|
||||
client_key=self.keyfile,
|
||||
client_cert=self.certfile,
|
||||
validate_cert=True,
|
||||
),
|
||||
)
|
||||
AsyncHTTPClient.configure(None, defaults={"ssl_options": ssl_context})
|
||||
return AsyncHTTPClient()
|
||||
|
||||
async def check_hub_version(self):
|
||||
|
@@ -1657,12 +1657,13 @@ class Spawner(LoggingConfigurable):
|
||||
raise NotImplementedError("Override in subclass. Must be a coroutine.")
|
||||
|
||||
def delete_forever(self):
|
||||
"""Called when a user or server is deleted.
|
||||
"""Called when a user or named server is deleted.
|
||||
|
||||
This can do things like request removal of resources such as persistent storage.
|
||||
Only called on stopped spawners, and is usually the last action ever taken for the user.
|
||||
Spawners must already be stopped before this method can be called.
|
||||
|
||||
Will only be called once on each Spawner, immediately prior to removal.
|
||||
Can be async.
|
||||
|
||||
Stopping a server does *not* call this method.
|
||||
"""
|
||||
|
@@ -62,7 +62,7 @@ async def test_submit_login_form(app, browser, user_special_chars):
|
||||
await browser.goto(login_url)
|
||||
await login(browser, user.name, password=user.name)
|
||||
expected_url = public_url(app, user)
|
||||
await expect(browser).to_have_url(expected_url)
|
||||
await browser.wait_for_url(expected_url)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -143,7 +143,7 @@ async def test_open_url_login(
|
||||
await expect(browser).to_have_url(re.compile(pattern))
|
||||
await expect(browser).not_to_have_url(re.compile(".*/user/.*"))
|
||||
else:
|
||||
await expect(browser).to_have_url(
|
||||
await browser.wait_for_url(
|
||||
re.compile(".*/user/" + f"{user_special_chars.urlname}/")
|
||||
)
|
||||
|
||||
@@ -883,17 +883,15 @@ async def test_menu_bar(app, browser, page, logged_in, user_special_chars):
|
||||
expected_url = f"hub/login?next={url_escape(app.base_url)}"
|
||||
assert expected_url in browser.url
|
||||
else:
|
||||
await expect(browser).to_have_url(
|
||||
await browser.wait_for_url(
|
||||
re.compile(f".*/user/{user_special_chars.urlname}/")
|
||||
)
|
||||
await browser.go_back()
|
||||
await expect(browser).to_have_url(re.compile(".*" + page))
|
||||
await browser.wait_for_url(re.compile(".*" + page))
|
||||
elif index == 3:
|
||||
await expect(browser).to_have_url(re.compile(".*/login"))
|
||||
await browser.wait_for_url(re.compile(".*/login"))
|
||||
else:
|
||||
await expect(browser).to_have_url(
|
||||
re.compile(".*" + expected_link_bar_url[index])
|
||||
)
|
||||
await browser.wait_for_url(re.compile(".*" + expected_link_bar_url[index]))
|
||||
|
||||
|
||||
# LOGOUT
|
||||
@@ -924,8 +922,8 @@ async def test_user_logout(app, browser, url, user_special_chars):
|
||||
|
||||
# verify that user can login after logout
|
||||
await login(browser, user.name, password=user.name)
|
||||
await expect(browser).to_have_url(
|
||||
re.compile(".*/user/" + f"{user_special_chars.urlname}/")
|
||||
await browser.wait_for_url(
|
||||
re.compile(".*/user/" + f"{user_special_chars.urlname}/"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1016,7 +1014,7 @@ async def test_oauth_page(
|
||||
await expect(scopes_element).not_to_be_visible()
|
||||
for scopes_element in scopes_elements
|
||||
]
|
||||
# checking that all scopes granded to user are presented in POST form (scope_list)
|
||||
# checking that all scopes granted to user are presented in POST form (scope_list)
|
||||
scope_list_oauth_page = [
|
||||
await scopes_element.get_attribute("value")
|
||||
for scopes_element in scopes_elements
|
||||
@@ -1288,8 +1286,8 @@ async def test_start_stop_server_on_admin_page(
|
||||
spawn_btn_xpath = f'//a[contains(@href, "spawn/{username}")]/button[contains(@class, "btn-light")]'
|
||||
spawn_btn = browser.locator(spawn_btn_xpath)
|
||||
await expect(spawn_btn).to_be_enabled()
|
||||
async with browser.expect_navigation(url=f"**/user/{username}/"):
|
||||
await spawn_btn.click()
|
||||
await spawn_btn.click()
|
||||
await browser.wait_for_url(url=f"**/user/{username}/")
|
||||
|
||||
async def click_access_server(browser, username):
|
||||
"""access to the server for users via the Access Server button"""
|
||||
@@ -1337,7 +1335,7 @@ async def test_start_stop_server_on_admin_page(
|
||||
|
||||
# click on Spawn page button
|
||||
await click_spawn_page(browser, user2.name)
|
||||
await expect(browser).to_have_url(re.compile(".*" + f"/user/{user2.name}/"))
|
||||
await browser.wait_for_url(re.compile(".*" + f"/user/{user2.name}/"))
|
||||
|
||||
# open/return to the Admin page
|
||||
admin_page = url_path_join(public_host(app), app.hub.base_url, "admin")
|
||||
@@ -1491,18 +1489,18 @@ async def test_singleuser_xsrf(
|
||||
await browser.goto(login_url)
|
||||
await login(browser, browser_user.name, browser_user.name)
|
||||
# end up at single-user
|
||||
await expect(browser).to_have_url(re.compile(rf".*/user/{browser_user.name}/.*"))
|
||||
await browser.wait_for_url(re.compile(rf".*/user/{browser_user.name}/.*"))
|
||||
# wait for target user to start, too
|
||||
await target_start
|
||||
await app.proxy.add_user(target_user)
|
||||
|
||||
# visit target user, sets credentials for second server
|
||||
await browser.goto(public_url(app, target_user))
|
||||
await expect(browser).to_have_url(re.compile(r".*/oauth2/authorize"))
|
||||
await browser.wait_for_url(re.compile(r".*/oauth2/authorize"))
|
||||
auth_button = browser.locator('//button[@type="submit"]')
|
||||
await expect(auth_button).to_be_enabled()
|
||||
await auth_button.click()
|
||||
await expect(browser).to_have_url(re.compile(rf".*/user/{target_user.name}/.*"))
|
||||
await browser.wait_for_url(re.compile(rf".*/user/{target_user.name}/.*"))
|
||||
|
||||
# at this point, we are on a page served by target_user,
|
||||
# logged in as browser_user
|
||||
@@ -1644,8 +1642,8 @@ async def test_singleuser_xsrf(
|
||||
url_path_join(app.base_url, f"hub/spawn/{browser_user.name}/{server_name}"),
|
||||
)
|
||||
await browser.goto(url)
|
||||
await expect(browser).to_have_url(
|
||||
re.compile(rf".*/user/{browser_user.name}/{server_name}/.*")
|
||||
await browser.wait_for_url(
|
||||
re.compile(rf".*/user/{browser_user.name}/{server_name}/.*"),
|
||||
)
|
||||
# from named server URL, make sure we can talk to a kernel
|
||||
token = browser_user.new_api_token(scopes=["access:servers!user"])
|
||||
|
@@ -102,7 +102,7 @@ def ssl_tmpdir(tmpdir_factory):
|
||||
|
||||
|
||||
@fixture(scope='module')
|
||||
async def app(request, io_loop, ssl_tmpdir):
|
||||
async def app(request, ssl_tmpdir):
|
||||
"""Mock a jupyterhub app for testing"""
|
||||
mocked_app = None
|
||||
ssl_enabled = getattr(
|
||||
@@ -170,39 +170,23 @@ async def io_loop(request):
|
||||
The main reason to depend on this fixture is to ensure your cleanup
|
||||
happens before the io_loop is closed.
|
||||
"""
|
||||
warn(
|
||||
"jupyterhub's io_loop fixture is deprecated. Use async fixtures to get the event loop.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
io_loop = AsyncIOMainLoop()
|
||||
event_loop = asyncio.get_running_loop()
|
||||
assert asyncio.get_event_loop() is event_loop
|
||||
assert io_loop.asyncio_loop is event_loop
|
||||
|
||||
def _close():
|
||||
# cleanup everything
|
||||
try:
|
||||
event_loop.run_until_complete(event_loop.shutdown_asyncgens())
|
||||
except (asyncio.CancelledError, RuntimeError):
|
||||
pass
|
||||
io_loop.close(all_fds=True)
|
||||
|
||||
# workaround pytest-asyncio trying to cleanup after loop is closed
|
||||
# problem introduced in pytest-asyncio 0.25.2
|
||||
def noop(*args, **kwargs):
|
||||
warn("Loop used after close...", RuntimeWarning, stacklevel=2)
|
||||
return
|
||||
|
||||
event_loop.run_until_complete = noop
|
||||
|
||||
request.addfinalizer(_close)
|
||||
return io_loop
|
||||
|
||||
|
||||
@fixture(autouse=True)
|
||||
async def cleanup_after(request, io_loop):
|
||||
async def cleanup_after(request):
|
||||
"""function-scoped fixture to shutdown user servers
|
||||
|
||||
allows cleanup of servers between tests
|
||||
without having to launch a whole new app
|
||||
|
||||
depends on io_loop to ensure it runs before things are closed.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@@ -382,6 +382,10 @@ class MockHub(JupyterHub):
|
||||
super().stop()
|
||||
self.db_file.close()
|
||||
|
||||
def _stop_event_loop(self):
|
||||
# leave it to pytest-asyncio to stop the loop
|
||||
pass
|
||||
|
||||
async def login_user(self, name):
|
||||
"""Login a user by name, returning her cookies."""
|
||||
base_url = public_url(self)
|
||||
|
@@ -37,6 +37,14 @@ def refresh_pre_spawn(app):
|
||||
app.authenticator.refresh_pre_spawn = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def refresh_pre_stop(app):
|
||||
"""Fixture enabling auth refresh pre stop"""
|
||||
app.authenticator.refresh_pre_stop = True
|
||||
yield
|
||||
app.authenticator.refresh_pre_stop = False
|
||||
|
||||
|
||||
async def test_auth_refresh_at_login(app, user):
|
||||
# auth_refreshed starts unset:
|
||||
assert not user._auth_refreshed
|
||||
@@ -175,3 +183,85 @@ async def test_refresh_pre_spawn_expired_admin_request(
|
||||
)
|
||||
# api requests can't do login redirects
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
async def test_refresh_pre_stop(app, user, refresh_pre_stop):
|
||||
cookies = await app.login_user(user.name)
|
||||
assert user._auth_refreshed
|
||||
user._auth_refreshed -= 10
|
||||
before = user._auth_refreshed
|
||||
|
||||
r = await api_request(
|
||||
app, f'users/{user.name}/server', method='post', name=user.name
|
||||
)
|
||||
|
||||
assert user._auth_refreshed == before
|
||||
assert 200 <= r.status_code < 300
|
||||
|
||||
# auth is fresh, but should be forced to refresh by stop
|
||||
r = await api_request(
|
||||
app, f'users/{user.name}/server', method='delete', name=user.name
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
assert user._auth_refreshed > before
|
||||
|
||||
|
||||
async def test_refresh_pre_stop_expired(app, user, refresh_pre_stop, disable_refresh):
|
||||
cookies = await app.login_user(user.name)
|
||||
assert user._auth_refreshed
|
||||
user._auth_refreshed -= 10
|
||||
before = user._auth_refreshed
|
||||
|
||||
r = await api_request(
|
||||
app, f'users/{user.name}/server', method='post', name=user.name
|
||||
)
|
||||
assert user._auth_refreshed == before
|
||||
assert 200 <= r.status_code < 300
|
||||
|
||||
# auth is fresh, doesn't trigger expiry
|
||||
r = await api_request(
|
||||
app, f'users/{user.name}/server', method='delete', name=user.name
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert user._auth_refreshed == before
|
||||
|
||||
|
||||
async def test_refresh_pre_stop_admin_request(app, user, admin_user, refresh_pre_stop):
|
||||
await app.login_user(user.name)
|
||||
await app.login_user(admin_user.name)
|
||||
user._auth_refreshed -= 10
|
||||
before = user._auth_refreshed
|
||||
|
||||
r = await api_request(
|
||||
app, 'users', user.name, 'server', method='post', name=admin_user.name
|
||||
)
|
||||
assert user._auth_refreshed == before
|
||||
assert 200 <= r.status_code < 300
|
||||
|
||||
# admin request, auth is fresh. Should still refresh user auth.
|
||||
r = await api_request(
|
||||
app, 'users', user.name, 'server', method='delete', name=admin_user.name
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
assert user._auth_refreshed > before
|
||||
|
||||
|
||||
async def test_refresh_pre_stop_expired_admin_request(
|
||||
app, user, admin_user, refresh_pre_stop, disable_refresh
|
||||
):
|
||||
await app.login_user(user.name)
|
||||
await app.login_user(admin_user.name)
|
||||
user._auth_refreshed -= 10
|
||||
|
||||
r = await api_request(
|
||||
app, 'users', user.name, 'server', method='post', name=admin_user.name
|
||||
)
|
||||
assert 200 <= r.status_code < 300
|
||||
|
||||
# auth needs refresh but can't without a new login; stop should be forced
|
||||
user._auth_refreshed -= app.authenticator.auth_refresh_age
|
||||
r = await api_request(
|
||||
app, 'users', user.name, 'server', method='delete', name=admin_user.name
|
||||
)
|
||||
|
||||
assert 200 <= r.status_code < 300
|
||||
|
@@ -65,7 +65,9 @@ async def test_proxy_service(app, mockservice_url):
|
||||
service = mockservice_url
|
||||
name = service.name
|
||||
await app.proxy.get_all_routes()
|
||||
url = public_url(app, service) + '/foo'
|
||||
url = public_url(app, service)
|
||||
assert url.endswith("/")
|
||||
url += "foo"
|
||||
r = await async_requests.get(url, allow_redirects=False)
|
||||
path = f'/services/{name}/foo'
|
||||
r.raise_for_status()
|
||||
|
@@ -29,11 +29,12 @@ async def yield_n(n, delay=0.01):
|
||||
yield i
|
||||
|
||||
|
||||
def schedule_future(io_loop, *, delay, result=None):
|
||||
def schedule_future(*, delay, result=None):
|
||||
"""Construct a Future that will resolve after a delay"""
|
||||
f = asyncio.Future()
|
||||
|
||||
if delay:
|
||||
io_loop.call_later(delay, lambda: f.set_result(result))
|
||||
asyncio.get_running_loop().call_later(delay, lambda: f.set_result(result))
|
||||
else:
|
||||
f.set_result(result)
|
||||
return f
|
||||
@@ -48,8 +49,8 @@ def schedule_future(io_loop, *, delay, result=None):
|
||||
(0.5, 10, 0.2, [0, 1]),
|
||||
],
|
||||
)
|
||||
async def test_iterate_until(io_loop, deadline, n, delay, expected):
|
||||
f = schedule_future(io_loop, delay=deadline)
|
||||
async def test_iterate_until(deadline, n, delay, expected):
|
||||
f = schedule_future(delay=deadline)
|
||||
|
||||
yielded = []
|
||||
async with aclosing(iterate_until(f, yield_n(n, delay=delay))) as items:
|
||||
@@ -58,8 +59,8 @@ async def test_iterate_until(io_loop, deadline, n, delay, expected):
|
||||
assert yielded == expected
|
||||
|
||||
|
||||
async def test_iterate_until_ready_after_deadline(io_loop):
|
||||
f = schedule_future(io_loop, delay=0)
|
||||
async def test_iterate_until_ready_after_deadline():
|
||||
f = schedule_future(delay=0)
|
||||
|
||||
async def gen():
|
||||
for i in range(5):
|
||||
|
@@ -170,7 +170,8 @@ def make_ssl_context(
|
||||
ssl_context.load_default_certs()
|
||||
|
||||
ssl_context.load_cert_chain(certfile, keyfile)
|
||||
ssl_context.check_hostname = check_hostname
|
||||
if check_hostname is not None:
|
||||
ssl_context.check_hostname = check_hostname
|
||||
return ssl_context
|
||||
|
||||
|
||||
@@ -983,3 +984,13 @@ def fmt_ip_url(ip):
|
||||
if ":" in ip:
|
||||
return f"[{ip}]"
|
||||
return ip
|
||||
|
||||
|
||||
def format_exception(exc, *, only_jupyterhub=False):
|
||||
"""
|
||||
Format an exception into a text string and HTML pair.
|
||||
"""
|
||||
default_message = None if only_jupyterhub else str(exc)
|
||||
return getattr(exc, "jupyterhub_message", default_message), getattr(
|
||||
exc, "jupyterhub_html_message", None
|
||||
)
|
||||
|
204
package-lock.json
generated
204
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"bootstrap": "^5.3.0",
|
||||
"jquery": "^3.5.1",
|
||||
"moment": "^2.29.4",
|
||||
@@ -21,18 +21,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.1.tgz",
|
||||
"integrity": "sha512-RLmb9U6H2rJDnGxEqXxzy7ANPrQz7WK2/eTjdZqyU9uRU5W+FkAec9uU5gTYzFBH7aoXIw2WTJSCJR4KPlReQw==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
|
||||
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
@@ -48,28 +51,30 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.4.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.4.1",
|
||||
"@parcel/watcher-darwin-x64": "2.4.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.4.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.4.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.4.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.4.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.4.1",
|
||||
"@parcel/watcher-win32-arm64": "2.4.1",
|
||||
"@parcel/watcher-win32-ia32": "2.4.1",
|
||||
"@parcel/watcher-win32-x64": "2.4.1"
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -83,13 +88,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -103,13 +109,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -123,13 +130,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -143,13 +151,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -163,13 +193,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -183,13 +214,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
|
||||
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -203,13 +235,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
|
||||
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -223,13 +256,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
|
||||
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -243,13 +277,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
|
||||
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -263,13 +298,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
|
||||
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -283,13 +319,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
|
||||
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -306,6 +343,7 @@
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -313,9 +351,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||
"version": "5.3.8",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -326,6 +364,7 @@
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
@@ -335,6 +374,7 @@
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -344,10 +384,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -363,6 +404,7 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
@@ -376,6 +418,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -385,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -396,6 +439,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -406,6 +450,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -419,6 +464,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -427,13 +473,15 @@
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -447,6 +495,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -456,6 +505,7 @@
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -463,6 +513,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -472,12 +523,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
|
||||
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
@@ -498,9 +550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.86.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.1.tgz",
|
||||
"integrity": "sha512-Yaok4XELL1L9Im/ZUClKu//D2OP1rOljKj0Gf34a+GzLbMveOzL7CfqYo+JUa5Xt1nhTCW+OcKp/FtR7/iqj1w==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -519,10 +571,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -532,6 +585,7 @@
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
@@ -19,7 +19,7 @@
|
||||
"sass": "^1.74.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"bootstrap": "^5.3.0",
|
||||
"jquery": "^3.5.1",
|
||||
"moment": "^2.29.4",
|
||||
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
# ref: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
||||
[project]
|
||||
name = "jupyterhub"
|
||||
version = "5.3.0"
|
||||
version = "5.4.0"
|
||||
dynamic = ["readme", "dependencies"]
|
||||
description = "JupyterHub: A multi-user server for Jupyter notebooks"
|
||||
authors = [
|
||||
@@ -147,7 +147,7 @@ indent_size = 2
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "5.3.0"
|
||||
current = "5.4.0"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
@@ -71,15 +71,21 @@ require(["jquery", "moment", "jhapi"], function ($, moment, JHAPI) {
|
||||
var row = getRow($(this));
|
||||
var serverName = row.data("server-name");
|
||||
|
||||
// before request
|
||||
disableRow(row);
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to delete your server "${serverName}"?`,
|
||||
)
|
||||
) {
|
||||
// before request
|
||||
disableRow(row);
|
||||
|
||||
// request
|
||||
api.delete_named_server(user, serverName, {
|
||||
success: function () {
|
||||
row.remove();
|
||||
},
|
||||
});
|
||||
// request
|
||||
api.delete_named_server(user, serverName, {
|
||||
success: function () {
|
||||
row.remove();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// initial state: hook up click events
|
||||
|
@@ -53,11 +53,8 @@
|
||||
* Font Awesome
|
||||
*
|
||||
*/
|
||||
$fa-font-path: "../components/@fortawesome/fontawesome-free/webfonts";
|
||||
@import "../components/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
// You can include all the other styles the same as before
|
||||
@import "../components/@fortawesome/fontawesome-free/scss/regular.scss";
|
||||
@import "../components/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
@import "../components/@fortawesome/fontawesome-free/css/fontawesome.css";
|
||||
@import "../components/@fortawesome/fontawesome-free/css/solid.css";
|
||||
|
||||
/*!
|
||||
*
|
||||
|
@@ -192,7 +192,7 @@
|
||||
<a id="logout"
|
||||
role="button"
|
||||
class="btn btn-sm btn-outline-contrast"
|
||||
href="{{ logout_url }}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
|
||||
href="{{ logout_url }}"> <i aria-hidden="true" class="fa fa-sign-out"></i>Logout</a>
|
||||
{% else %}
|
||||
<a id="login"
|
||||
role="button"
|
||||
|
@@ -14,7 +14,11 @@
|
||||
{% if for_user and user.name != for_user.name -%}
|
||||
<p>Spawning server for {{ for_user.name }}</p>
|
||||
{% endif -%}
|
||||
{% if error_message -%}<p class="spawn-error-msg alert alert-danger">Error: {{ error_message }}</p>{% endif %}
|
||||
{% if error_message %}
|
||||
<p class="spawn-error-msg alert alert-danger">Error: {{ error_message }}</p>
|
||||
{% elif error_html_message %}
|
||||
<p class="spawn-error-msg alert alert-danger">{{ error_html_message | safe }}</p>
|
||||
{% endif %}
|
||||
<form enctype="multipart/form-data"
|
||||
id="spawn_form"
|
||||
action="{{ url | safe }}"
|
||||
|
Reference in New Issue
Block a user