Compare commits

...

121 Commits

Author SHA1 Message Date
Min RK
b186bdbce3 Bump to 2.0.0rc5 2021-11-26 09:07:15 +01:00
Min RK
36fe6c6f66 Merge pull request #3692 from minrk/clrc5
changelog for 2.0.0rc5
2021-11-26 09:06:21 +01:00
Min RK
8bf559db52 changelog for 2.0.0rc5 2021-11-26 09:05:21 +01:00
Simon Li
750085f627 Merge pull request #3690 from minrk/gha-singleuser
build jupyterhub/singleuser along with other images
2021-11-25 20:17:12 +00:00
Min RK
2dc2c99b4a Merge pull request #3640 from minrk/doc-api-only
add api-only doc
2021-11-25 20:26:25 +01:00
pre-commit-ci[bot]
e703555888 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-11-25 19:16:41 +00:00
Min RK
7e102f0511 Apply suggestions from code review
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
2021-11-25 20:16:10 +01:00
Min RK
facde96425 build jupyterhub/singleuser along with other images
got lost in the migration to GHA docker builds
2021-11-24 21:15:59 +01:00
Erik Sundell
608c746a59 Merge pull request #3689 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-11-22 22:26:25 +01:00
pre-commit-ci[bot]
a8c834410f [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.29.0 → v2.29.1](https://github.com/asottile/pyupgrade/compare/v2.29.0...v2.29.1)
- [github.com/psf/black: 21.10b0 → 21.11b1](https://github.com/psf/black/compare/21.10b0...21.11b1)
2021-11-22 20:51:45 +00:00
Min RK
bda14b487a Bump to 2.0.0rc4 2021-11-18 15:33:12 +01:00
Min RK
fd5cf8c360 Merge pull request #3687 from minrk/rc4-changelog
update 2.0 changelog
2021-11-18 15:32:27 +01:00
Min RK
03758e5b46 update 2.0 changelog
prep for rc4
2021-11-18 14:50:10 +01:00
Erik Sundell
e540d143bb Merge pull request #3685 from minrk/session-id-model
Add Session id to token/identify models
2021-11-18 13:39:34 +01:00
Erik Sundell
b2c5ad40c5 Merge pull request #3686 from minrk/login_with_token
Hub: only accept tokens in API requests
2021-11-18 13:27:41 +01:00
Min RK
edfdf672d8 Hub: only accept tokens in API requests
do not allow token-based access to pages

Tokens are only accepted via Authorization header, which doesn't make sense to pass to pages,
so disallow it explicitly to avoid surprises
2021-11-18 09:36:49 +01:00
Min RK
39f19aef49 add session_id to token model 2021-11-17 09:46:26 +01:00
Min RK
8813bb63d4 update to openapi 3.0
easier to implement oneOf schemas

document scopes, session_id in /api/user model
2021-11-17 09:44:38 +01:00
Yuvi Panda
7c18d6fe14 Merge pull request #3681 from minrk/log-app-versions
Log single-user app versions at startup
2021-11-16 00:11:32 +05:30
Erik Sundell
d1fe17d3cb Merge pull request #3682 from minrk/relpath
always use relative paths in data_files
2021-11-08 14:06:20 +01:00
Min RK
b8965c2017 always use relative paths in data_files
instead of absolute paths for sources

seems to have started failing on Windows
2021-11-08 13:29:26 +01:00
Min RK
733d7bc158 Log single-user app versions at startup
Reports jupyterlab, jupyter_server versions during startup
2021-11-08 09:21:32 +01:00
Min RK
88f31c29bb add api-only doc
Describe how to use JupyterHub as a "behind the scenes" implementation detail,
rather than a user-facing application.
2021-11-04 17:16:59 +01:00
Min RK
3caf3cfda8 Bump to 2.0.0rc3 2021-11-04 15:52:37 +01:00
Erik Sundell
d076c55cca Merge pull request #3679 from minrk/forward-1.5
Forward-port fixes from 1.5.0 security release
2021-11-04 15:30:04 +01:00
Min RK
3e185022c8 changelog for 1.5.0 2021-11-04 15:04:40 +01:00
Min RK
857ee2885f jupyterlab: don't use $JUPYTERHUB_API_TOKEN in PageConfig.token 2021-11-04 15:03:12 +01:00
Min RK
cd8dd56213 Revert "store tokens passed via url or header, not only url."
This reverts commit 53c3201c17.

Only tokens in URLs should be persisted in cookies.
Tokens in headers should not have any effect on cookies.
2021-11-04 15:03:12 +01:00
Erik Sundell
f06902aa8f Merge pull request #3675 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-11-02 01:56:07 +01:00
pre-commit-ci[bot]
bb109c6f75 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.9b0 → 21.10b0](https://github.com/psf/black/compare/21.9b0...21.10b0)
2021-11-01 20:25:25 +00:00
Erik Sundell
e525ec7b5b Merge pull request #3674 from minrk/verify-login-role
verify that successful login assigns default role
2021-10-30 17:50:01 +02:00
Min RK
356b98e19f verify that successful login assigns default role
and that repeated login after revoked role doesn't reassign role
2021-10-30 14:30:33 +02:00
Erik Sundell
8c803e7a53 Merge pull request #3673 from minrk/main
more calculators
2021-10-30 14:21:17 +02:00
Min RK
2e21a6f4e0 more calculators 2021-10-30 14:07:04 +02:00
Min RK
cfd31b14e3 Bump to 2.0.0rc2 2021-10-30 13:36:54 +02:00
Erik Sundell
f03a620424 Merge pull request #3672 from minrk/prerelease
use v2 of jupyterhub/action-major-minor-tag-calculator
2021-10-30 13:29:43 +02:00
Min RK
440ad77ad5 use v2 of jupyterhub/action-major-minor-tag-calculator
needed for handling of prerelease tags
2021-10-30 12:42:29 +02:00
Min RK
68835e97a2 Bump to 2.0.0rc1 2021-10-30 12:37:39 +02:00
Min RK
ce80c9c9cf Merge pull request #3669 from minrk/bumpversion
use tbump to tag versions
2021-10-30 12:34:28 +02:00
Min RK
3c299fbfb7 use tbump for bumping versions
avoids needing to manually keep files in sync,
and dramatically reduces RELEASE steps
2021-10-30 12:18:14 +02:00
Min RK
597f8ea6eb Merge pull request #3670 from manics/support-bot
Add support-bot
2021-10-30 12:17:47 +02:00
Erik Sundell
d1181085bf Merge pull request #3665 from minrk/openapi-test
Tests for our openapi spec
2021-10-29 16:05:05 +02:00
Simon Li
913832da48 Add support-bot
The old support-bot was disabled https://github.com/jupyterhub/.github/issues/15
This is the recommended replacement https://github.com/dessant/support-requests/issues/8
2021-10-29 14:09:49 +01:00
Min RK
42f57f4a72 Merge pull request #3662 from minrk/2.0rc-changelog
changelog for 2.0 release candidate
2021-10-29 13:40:34 +02:00
Min RK
d01a518c41 add updating rest-api version to release
and make real release checklist, to match other repos
2021-10-29 13:13:41 +02:00
Min RK
65ce06b116 test feedback
use global PYTEST_ARGS for nicer, simpler always-on arguments for pytest
2021-10-29 13:13:41 +02:00
Min RK
468aa5e93c render openapi spec client-side
- move spec to _static/rest-api.yml, since the original yaml must be served
- copy javascript rendering code from FastAPI (uses swagger-ui)
- remove link to pet store, since there isn't a big enough difference to duplicate it
- remove bootprint rendering with node
2021-10-29 13:13:41 +02:00
Min RK
5c01370e6f set version as long as we are rewriting the file 2021-10-29 13:13:41 +02:00
Min RK
21d08883a8 resolve rest-api validation errors
- regen scopes by running generate-scopes.py
2021-10-29 13:13:41 +02:00
Min RK
59de506f20 validate the rest-api
both with github action that runs openapi validation,
and a couple local tests to verify some maintenance tasks are done
2021-10-29 13:13:41 +02:00
Erik Sundell
b34120ed81 Merge pull request #3663 from minrk/clarify-default-roles
clarify some log messages during role assignment
2021-10-29 12:19:45 +02:00
Erik Sundell
617978179d Merge pull request #3667 from minrk/autodoc-traits
use stable autodoc-traits
2021-10-29 12:16:30 +02:00
Min RK
0985d6fdf2 use stable autodoc-traits 2021-10-29 11:32:02 +02:00
Min RK
2049fb0491 clarify some log messages during role assignment
and only commit on change
2021-10-29 11:22:12 +02:00
Erik Sundell
a58fc6534b Merge pull request #3664 from minrk/create-groups-on-startup
create groups declared in roles
2021-10-28 03:30:25 +02:00
Min RK
a14f97b7aa create groups declared in roles
matches behavior of users
2021-10-27 21:00:49 +02:00
Min RK
0a4cd5b4f2 add auto-changelog for 2.0rc 2021-10-27 16:22:43 +02:00
Min RK
dca6d372df Merge pull request #3661 from minrk/owner-metascope
Rename 'all' metascope to more descriptive 'inherit'
2021-10-27 16:20:29 +02:00
pre-commit-ci[bot]
3898c72921 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-10-27 14:01:05 +00:00
Min RK
b25517efe8 Rename 'all' metascope to more descriptive 'inherit'
since it means 'inheriting' the owner's permissions

'all' prompted the question 'all of what, exactly?'

Additionally, fix some NameErrors that should have been KeyErrors
2021-10-27 16:00:21 +02:00
Erik Sundell
392dffd11e Merge pull request #3659 from minrk/deprecate-admin-only
deprecate instead of remove `@admin_only` auth decorator
2021-10-25 23:32:01 +02:00
Erik Sundell
510f6ea7e6 Merge pull request #3660 from minrk/scope-error
minor refinement of excessive scopes error message
2021-10-25 15:53:43 +02:00
Min RK
296a0ad2f2 minor refinement of excessive scopes error message
show the role name
2021-10-25 14:10:57 +02:00
Min RK
487c4524ad deprecate instead of remove @admin_only auth decorator
no harm in keeping it around for a deprecation cycle
2021-10-25 13:00:45 +02:00
Erik Sundell
b2f0208fcc Merge pull request #3658 from minrk/better-timeout-message
improve timeout handling and messages
2021-10-20 22:15:26 +02:00
Min RK
84b9c3848c more detailed error messages for start timeouts
these are the most common error for any number of reasons spawn may fail
2021-10-20 20:08:34 +02:00
Min RK
9adbafdfb3 consistent handling of any timeout error
some things raise standard TimeoutError, others may raise tornado gen.TimeoutError (gen.with_timeout)

For consistency, add AnyTimeoutError tuple to allow catching any timeout, no matter what kind

Where we were raising `TimeoutError`,
we should have been raising `asyncio.TimeoutError`.

The base TimeoutError is an OSError for ETIMEO, which is for system calls
2021-10-20 20:07:45 +02:00
Erik Sundell
9cf2b5101e Merge pull request #3657 from edgarcosta/patch-1
docs: fix typo in proxy config example
2021-10-20 09:12:30 +02:00
Edgar Costa
725fa3a48a typo 2021-10-19 22:39:41 -04:00
Erik Sundell
534dda3dc7 Merge pull request #3653 from minrk/admin-no-such-user
raise 404 on admin attempt to spawn nonexistent user
2021-10-15 15:23:18 +02:00
Min RK
b0c7df04ac raise 404 on admin attempt to spawn nonexistent user 2021-10-15 14:40:47 +02:00
Min RK
61b0e8bef5 2.0.0b3 2021-10-14 12:49:20 +02:00
Erik Sundell
64f3938528 Merge pull request #3649 from minrk/cl-beta-3
add 424 status code change to changelog
2021-10-13 09:52:33 +02:00
Min RK
85bc92d88e Merge pull request #3646 from joegasewicz/joegasewicz-New-user-token-returns-200-instead-of-201-1
new user token returns 200 instead of 201
2021-10-13 09:24:30 +02:00
Min RK
7bcda18564 add 424 status code change to changelog 2021-10-13 09:17:47 +02:00
Erik Sundell
86da36857e Merge pull request #3647 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-10-12 00:13:22 +02:00
pre-commit-ci[bot]
530833e930 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1)
2021-10-11 19:42:10 +00:00
Joe Gasewicz
3b0850fa9b Fixed test_roles 2021-10-07 23:14:10 +01:00
josefgasewicz
1366911be6 Fixed tests & set status after writing json 2021-10-07 22:21:16 +01:00
Joe Gasewicz
fe276eac64 Update users.py
New user token returns 200 instead of 201 Fixes #3642
2021-10-07 16:31:23 +01:00
Min RK
9209ccd0de Merge pull request #3636 from yuvipanda/404
Fail suspected API requests with 424, not 503
2021-10-05 15:16:18 +02:00
YuviPanda
3b2a1a37f9 Update tests that were looking for 503s 2021-10-05 18:10:52 +05:30
YuviPanda
6007ba78b0 Preserve older 503 behavior behind a flag 2021-10-05 17:56:51 +05:30
YuviPanda
9cb19cc342 Use 424 rather than 404 to indicate non-running server
404 is also used to identify that a particular resource
(like a kernel or terminal) is not present, maybe because
it is deleted. That comes from the notebook server, while
here we are responding from JupyterHub. Saying that the
user server they are trying to request the resource (kernel, etc)
from does not exist seems right.
2021-10-05 17:44:17 +05:30
YuviPanda
0f471f4e12 Fail suspected API requests with 404, not 503
Non-running user servers making requests is a fairly
common occurance - user servers get culled while their
browser tabs are left open. So we now have a background level
of 503s responses on the hub *all* the time, making it
very difficult to detect *real* 503s, which should ideally
be closely monitored and alerted on.

I *think* 404 is a more appropriate response, as the resource
(API) being requested is no longer present.
2021-10-05 03:00:16 +05:30
Erik Sundell
68db740998 Merge pull request #3635 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-10-04 22:38:05 +02:00
pre-commit-ci[bot]
9c0c6f25b7 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.28.0 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.28.0...v2.29.0)
2021-10-04 19:48:13 +00:00
Min RK
5f0077cb5b Merge pull request #3445 from rpwagner/patch-1
Initial SECURITY.md
2021-09-29 09:42:59 +02:00
Min RK
a6a2056cca 2.0.0b2 2021-09-29 09:41:57 +02:00
Erik Sundell
fb1e81212f Merge pull request #3628 from minrk/beta-2
add latest changes to 2.0 changelog
2021-09-28 18:32:14 +02:00
Min RK
17f811d0b4 add latest changes to 2.0 changelog
- nullauthenticator
- lab by default
2021-09-28 15:28:59 +02:00
Min RK
34398d94de Merge pull request #3627 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-28 15:23:00 +02:00
Min RK
6bf94fde48 extend deadline for docker build test
it's building 4 images, 10 minutes isn't always enough, bump to 20
2021-09-28 14:46:33 +02:00
Min RK
ee18fed04b Merge pull request #3619 from manics/nullauthenticator
Add NullAuthenticator to jupyterhub
2021-09-28 11:36:41 +02:00
Simon Li
28f56ba510 Simplify NullAuthenticator, add test 2021-09-27 23:05:53 +01:00
pre-commit-ci[bot]
c8d3dbb7b1 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-09-27 19:45:58 +00:00
pre-commit-ci[bot]
a76a093638 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.26.0 → v2.28.0](https://github.com/asottile/pyupgrade/compare/v2.26.0...v2.28.0)
2021-09-27 19:43:18 +00:00
Erik Sundell
27908a8e17 Merge pull request #3616 from minrk/delete-scopes
add delete scopes for users, groups, servers
2021-09-27 14:01:51 +02:00
Erik Sundell
8a30f015c9 Merge pull request #3626 from minrk/token-log
server-api example typo: trim space in token file
2021-09-27 12:51:07 +02:00
Min RK
8cac83fc96 add delete scopes for users, groups, servers
e.g. cull-idle services do not need permission to start servers in order to be able to stop them
2021-09-27 12:43:56 +02:00
Min RK
9ade4bb9b2 server-api example: trim space in token file
avoids invalid newlines in the auth header
2021-09-27 12:42:23 +02:00
Min RK
874c91a086 Merge pull request #3615 from minrk/lab-by-default
2.0: jupyterlab by default
2021-09-27 12:39:31 +02:00
Min RK
a906677440 Merge pull request #3625 from albertmichaelj/main
Added base_url to path for jupyterhub-session-id cookie
2021-09-27 12:27:15 +02:00
pre-commit-ci[bot]
3f93942a24 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-09-26 19:55:05 +00:00
Michael Albert
aeb3130b25 Added base_url to path for jupyterhub_session_id cookie 2021-09-26 15:33:08 -04:00
Simon Li
8a6b364ca5 nullauthenticator: missing import 2021-09-23 20:40:00 +01:00
Simon Li
2ade7328d1 nullauthenticator: relative imports, entrypoint, doc 2021-09-23 20:39:54 +01:00
Min RK
2bb9f4f444 implement null authenticator 2021-09-23 19:14:07 +01:00
Simon Li
b029d983f9 Merge pull request #3607 from minrk/recommend-nodesource
update quickstart requirements
2021-09-23 17:31:18 +01:00
Min RK
4082006039 2.0: jupyterlab by default
swaps from default nbclassic and opt-in to lab, to now default to lab and opt-in to nbclassic

defaults to jupyterlab *if* lab 3.1 is available,
so should still work without configuration if lab is unavailable (or too old)
2021-09-23 14:52:14 +02:00
Min RK
69aa0eaa7a update quickstart requirements
- remove mention of outdated nodejs-legacy
- mention nodesource for more recent node
- mention jupyterlab
- initial localhost request will be on http, not https
2021-09-23 13:59:21 +02:00
Min RK
3674ada640 Merge pull request #3614 from jupyterhub/dependabot/npm_and_yarn/jsx/nth-check-2.0.1
Bump nth-check from 2.0.0 to 2.0.1 in /jsx
2021-09-23 13:56:00 +02:00
dependabot[bot]
48accb0a64 Bump nth-check from 2.0.0 to 2.0.1 in /jsx
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-22 09:01:52 +00:00
Simon Li
70ac143cfe Merge pull request #3613 from jupyterhub/dependabot/npm_and_yarn/jsx/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5 in /jsx
2021-09-22 10:01:15 +01:00
dependabot[bot]
b1b2d531f8 Bump tmpl from 1.0.4 to 1.0.5 in /jsx
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-22 07:21:19 +00:00
Simon Li
e200783c59 Merge pull request #3610 from mriedem/patch-1
Fix 1.3 level
2021-09-20 21:28:54 +01:00
Erik Sundell
a7e57196c6 Merge pull request #3611 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-09-20 22:16:29 +02:00
pre-commit-ci[bot]
b5f05e6cd2 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 21.8b0 → 21.9b0](https://github.com/psf/black/compare/21.8b0...21.9b0)
- [github.com/pre-commit/mirrors-prettier: v2.4.0 → v2.4.1](https://github.com/pre-commit/mirrors-prettier/compare/v2.4.0...v2.4.1)
2021-09-20 19:57:49 +00:00
Matt Riedemann
5fe5b35f21 Fix 1.3 level
The 1.3 section was a sub-section of 1.4
which makes it harder to find 1.3 release notes
in the changelog from the navigation.
2021-09-20 13:40:45 -05:00
Rick Wagner
3610454a12 adding initial security policy 2021-06-01 09:51:20 -07:00
Rick Wagner
abc4bbebe4 Initial SECURITY.md
Proposing a basic security policy, similar to the README or contributors guide, based on the [GitHub documentation](https://docs.github.com/en/code-security/security-advisories/adding-a-security-policy-to-your-repository) and current Project Jupyter recommendations. This may be better as a [default file for the organization](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file).
2021-04-23 23:12:51 -07:00
67 changed files with 2589 additions and 1504 deletions

View File

@@ -129,7 +129,7 @@ jobs:
# If GITHUB_TOKEN isn't available (e.g. in PRs) returns no tags [].
- name: Get list of jupyterhub tags
id: jupyterhubtags
uses: jupyterhub/action-major-minor-tag-calculator@v1
uses: jupyterhub/action-major-minor-tag-calculator@v2
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub:"
@@ -150,7 +150,7 @@ jobs:
- name: Get list of jupyterhub-onbuild tags
id: onbuildtags
uses: jupyterhub/action-major-minor-tag-calculator@v1
uses: jupyterhub/action-major-minor-tag-calculator@v2
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:"
@@ -171,7 +171,7 @@ jobs:
- name: Get list of jupyterhub-demo tags
id: demotags
uses: jupyterhub/action-major-minor-tag-calculator@v1
uses: jupyterhub/action-major-minor-tag-calculator@v2
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:"
@@ -190,3 +190,23 @@ jobs:
platforms: linux/amd64
push: true
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
# jupyterhub/singleuser
- name: Get list of jupyterhub/singleuser tags
id: singleusertags
uses: jupyterhub/action-major-minor-tag-calculator@v2
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
prefix: "${{ env.REGISTRY }}jupyterhub/singleuser:"
defaultTag: "${{ env.REGISTRY }}jupyterhub/singleuser:noref"
branchRegex: ^\w[\w-.]*$
- name: Build and push jupyterhub/singleuser
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
with:
build-args: |
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
context: singleuser
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ join(fromJson(steps.singleusertags.outputs.tags)) }}

31
.github/workflows/support-bot.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# https://github.com/dessant/support-requests
name: "Support Requests"
on:
issues:
types: [labeled, unlabeled, reopened]
permissions:
issues: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: "support"
issue-comment: |
Hi there @{issue-author} :wave:!
I closed this issue because it was labelled as a support question.
Please help us organize discussion by posting this on the http://discourse.jupyter.org/ forum.
Our goal is to sustain a positive experience for both users and developers. We use GitHub issues for specific discussions related to changing a repository's content, and let the forum be where we can more generally help and inspire each other.
Thanks you for being an active member of our community! :heart:
close-issue: true
lock-issue: false
issue-lock-reason: "off-topic"

View File

@@ -14,8 +14,28 @@ on:
env:
# UTF-8 content may be interpreted as ascii and causes errors without this.
LANG: C.UTF-8
PYTEST_ADDOPTS: "--verbose --color=yes"
jobs:
rest-api:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Validate REST API
uses: char0n/swagger-editor-validate@182d1a5d26ff5c2f4f452c43bd55e2c7d8064003
with:
definition-file: docs/source/_static/rest-api.yml
- uses: actions/setup-python@v2
with:
python-version: "3.9"
# in addition to the doc requirements
# the docs *tests* require pre-commit and pytest
- run: |
pip install -r docs/requirements.txt pytest pre-commit -e .
- run: |
pytest docs/
# Run "pytest jupyterhub/tests" in various configurations
pytest:
runs-on: ubuntu-20.04
@@ -38,9 +58,9 @@ jobs:
# Tests everything when JupyterHub works against a dedicated mysql or
# postgresql server.
#
# jupyter_server:
# nbclassic:
# Tests everything when the user instances are started with
# jupyter_server instead of notebook.
# notebook instead of jupyter_server.
#
# ssl:
# Tests everything using internal SSL connections instead of
@@ -48,7 +68,7 @@ jobs:
#
# main_dependencies:
# Tests everything when the we use the latest available dependencies
# from: ipytraitlets.
# from: traitlets.
#
# NOTE: Since only the value of these parameters are presented in the
# GitHub UI when the workflow run, we avoid using true/false as
@@ -56,6 +76,7 @@ jobs:
include:
- python: "3.6"
oldest_dependencies: oldest_dependencies
nbclassic: nbclassic
- python: "3.6"
subdomain: subdomain
- python: "3.7"
@@ -65,7 +86,7 @@ jobs:
- python: "3.8"
db: postgres
- python: "3.8"
jupyter_server: jupyter_server
nbclassic: nbclassic
- python: "3.9"
main_dependencies: main_dependencies
@@ -130,9 +151,9 @@ jobs:
if [ "${{ matrix.main_dependencies }}" != "" ]; then
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
fi
if [ "${{ matrix.jupyter_server }}" != "" ]; then
pip uninstall notebook --yes
pip install jupyter_server
if [ "${{ matrix.nbclassic }}" != "" ]; then
pip uninstall jupyter_server --yes
pip install notebook
fi
if [ "${{ matrix.db }}" == "mysql" ]; then
pip install mysql-connector-python
@@ -181,10 +202,8 @@ jobs:
fi
- name: Run pytest
# FIXME: --color=yes explicitly set because:
# https://github.com/actions/runner/issues/241
run: |
pytest -v --maxfail=2 --color=yes --cov=jupyterhub jupyterhub/tests
pytest --maxfail=2 --cov=jupyterhub jupyterhub/tests
- name: Run yarn jest test
run: |
cd jsx && yarn && yarn test
@@ -194,7 +213,7 @@ jobs:
docker-build:
runs-on: ubuntu-20.04
timeout-minutes: 10
timeout-minutes: 20
steps:
- uses: actions/checkout@v2

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.26.0
rev: v2.29.1
hooks:
- id: pyupgrade
args:
@@ -10,15 +10,15 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/psf/black
rev: 21.8b0
rev: 21.11b1
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.4.0
rev: v2.4.1
hooks:
- id: prettier
- repo: https://github.com/PyCQA/flake8
rev: "3.9.2"
rev: "4.0.1"
hooks:
- id: flake8
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@@ -1,26 +0,0 @@
# Release checklist
- [ ] Upgrade Docs prior to Release
- [ ] Change log
- [ ] New features documented
- [ ] Update the contributor list - thank you page
- [ ] Upgrade and test Reference Deployments
- [ ] Release software
- [ ] Make sure 0 issues in milestone
- [ ] Follow release process steps
- [ ] Send builds to PyPI (Warehouse) and Conda Forge
- [ ] Blog post and/or release note
- [ ] Notify users of release
- [ ] Email Jupyter and Jupyter In Education mailing lists
- [ ] Tweet (optional)
- [ ] Increment the version number for the next release
- [ ] Update roadmap

View File

@@ -56,9 +56,11 @@ Basic principles for operation are:
servers.
JupyterHub also provides a
[REST API](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/HEAD/docs/rest-api.yml#/default)
[REST API][]
for administration of the Hub and its users.
[rest api]: https://juptyerhub.readthedocs.io/en/latest/reference/rest-api.html
## Installation
### Check prerequisites
@@ -239,7 +241,7 @@ You can also talk with us on our JupyterHub [Gitter](https://gitter.im/jupyterhu
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
- [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial)
- [Documentation for JupyterHub](https://jupyterhub.readthedocs.io/en/latest/) | [PDF (latest)](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf) | [PDF (stable)](https://media.readthedocs.org/pdf/jupyterhub/stable/jupyterhub.pdf)
- [Documentation for JupyterHub's REST API](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/HEAD/docs/rest-api.yml#/default)
- [Documentation for JupyterHub's REST API][rest api]
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)
- [Project Jupyter website](https://jupyter.org)
- [Project Jupyter community](https://jupyter.org/community)

50
RELEASE.md Normal file
View File

@@ -0,0 +1,50 @@
# How to make a release
`jupyterhub` is a package [available on
PyPI](https://pypi.org/project/jupyterhub/) and
[conda-forge](https://conda-forge.org/).
These are instructions on how to make a release on PyPI.
The PyPI release is done automatically by CI when a tag is pushed.
For you to follow along according to these instructions, you need:
- To have push rights to the [jupyterhub GitHub
repository](https://github.com/jupyterhub/jupyterhub).
## Steps to make a release
1. Checkout main and make sure it is up to date.
```shell
ORIGIN=${ORIGIN:-origin} # set to the canonical remote, e.g. 'upstream' if 'origin' is not the official repo
git checkout main
git fetch $ORIGIN main
git reset --hard $ORIGIN/main
```
1. Make sure `docs/source/changelog.md` is up-to-date.
[github-activity][] can help with this.
1. Update the version with `tbump`.
You can see what will happen without making any changes with `tbump --dry-run ${VERSION}`
```shell
tbump ${VERSION}
```
This will tag and publish a release,
which will be finished on CI.
1. Reset the version back to dev, e.g. `2.1.0.dev` after releasing `2.0.0`
```shell
tbump --no-tag ${NEXT_VERSION}.dev
```
1. Following the release to PyPI, an automated PR should arrive to
[conda-forge/jupyterhub-feedstock][],
check for the tests to succeed on this PR and then merge it to successfully
update the package for `conda` on the conda-forge channel.
[github-activity]: https://github.com/choldgraf/github-activity
[conda-forge/jupyterhub-feedstock]: https://github.com/conda-forge/jupyterhub-feedstock

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Reporting a Vulnerability
If you believe youve 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).

View File

@@ -7,13 +7,14 @@ codecov
coverage
cryptography
html5lib # needed for beautifulsoup
jupyterlab >=3
mock
notebook
pre-commit
pytest>=3.3
pytest-asyncio
pytest-cov
requests-mock
tbump
# blacklist urllib3 releases affected by https://github.com/urllib3/urllib3/issues/1683
# I *think* this should only affect testing, not production
urllib3!=1.25.4,!=1.25.5

View File

@@ -53,14 +53,6 @@ help:
clean:
rm -rf $(BUILDDIR)/*
node_modules: package.json
npm install && touch node_modules
rest-api: source/_static/rest-api/index.html
source/_static/rest-api/index.html: rest-api.yml node_modules
npm run rest-api
metrics: source/reference/metrics.rst
source/reference/metrics.rst: generate-metrics.py
@@ -71,7 +63,7 @@ scopes: source/rbac/scope-table.md
source/rbac/scope-table.md: source/rbac/generate-scope-table.py
python3 source/rbac/generate-scope-table.py
html: rest-api metrics scopes
html: metrics scopes
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

View File

@@ -1,14 +0,0 @@
{
"name": "jupyterhub-docs-build",
"version": "0.8.0",
"description": "build JupyterHub swagger docs",
"scripts": {
"rest-api": "bootprint openapi ./rest-api.yml source/_static/rest-api"
},
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"bootprint": "^1.0.0",
"bootprint-openapi": "^1.0.0"
}
}

View File

@@ -1,9 +1,7 @@
-r ../requirements.txt
alabaster_jupyterhub
# Temporary fix of #3021. Revert back to released autodoc-traits when
# 0.1.0 released.
https://github.com/jupyterhub/autodoc-traits/archive/d22282c1c18c6865436e06d8b329c06fe12a07f8.zip
autodoc-traits
myst-parser
pydata-sphinx-theme
pytablewriter>=0.56

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,9 @@
.navbar-brand {
height: 4rem !important;
}
/* hide redundant funky-formatted swagger-ui version */
.swagger-ui .info .title small {
display: none !important;
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,11 +17,6 @@ information on:
- making an API request programmatically using the requests library
- learning more about JupyterHub's API
The same JupyterHub API spec, as found here, is available in an interactive form
`here (on swagger's petstore) <https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#!/default>`__.
The `OpenAPI Initiative`_ (fka Swagger™) is a project used to describe
and document RESTful APIs.
JupyterHub API Reference:
.. toctree::

File diff suppressed because one or more lines are too long

View File

@@ -215,7 +215,7 @@ if on_rtd:
# build both metrics and rest-api, since RTD doesn't run make
from subprocess import check_call as sh
sh(['make', 'metrics', 'rest-api', 'scopes'], cwd=docs)
sh(['make', 'metrics', 'scopes'], cwd=docs)
# -- Spell checking -------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -43,7 +43,7 @@ JupyterHub performs the following functions:
notebook servers
For convenient administration of the Hub, its users, and services,
JupyterHub also provides a `REST API`_.
JupyterHub also provides a :doc:`REST API <reference/rest-api>`.
The JupyterHub team and Project Jupyter value our community, and JupyterHub
follows the Jupyter `Community Guides <https://jupyter.readthedocs.io/en/latest/community/content-community.html>`_.
@@ -155,4 +155,3 @@ Questions? Suggestions?
.. _JupyterHub: https://github.com/jupyterhub/jupyterhub
.. _Jupyter notebook: https://jupyter-notebook.readthedocs.io/en/latest/
.. _REST API: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#!/default

View File

@@ -5,8 +5,8 @@
Before installing JupyterHub, you will need:
- a Linux/Unix based system
- [Python](https://www.python.org/downloads/) 3.5 or greater. An understanding
of using [`pip`](https://pip.pypa.io/en/stable/) or
- [Python](https://www.python.org/downloads/) 3.6 or greater. An understanding
of using [`pip`](https://pip.pypa.io) or
[`conda`](https://conda.io/docs/get-started.html) for
installing Python packages is helpful.
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
@@ -20,11 +20,11 @@ Before installing JupyterHub, you will need:
For example, install it on Linux (Debian/Ubuntu) using:
```
sudo apt-get install npm nodejs-legacy
sudo apt-get install nodejs npm
```
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
[nodesource][] is a great resource to get more recent versions of the nodejs runtime,
if your system package manager only has an old version of Node.js (e.g. 10 or older).
- A [pluggable authentication module (PAM)](https://en.wikipedia.org/wiki/Pluggable_authentication_module)
to use the [default Authenticator](./getting-started/authenticators-users-basics.md).
@@ -33,11 +33,17 @@ Before installing JupyterHub, you will need:
- TLS certificate and key for HTTPS communication
- Domain name
[nodesource]: https://github.com/nodesource/distributions#table-of-contents
Before running the single-user notebook servers (which may be on the same
system as the Hub or not), you will need:
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
version 4 or greater
- [JupyterLab][] version 3 or greater,
or [Jupyter Notebook][]
4 or greater.
[jupyterlab]: https://jupyterlab.readthedocs.io
[jupyter notebook]: https://jupyter.readthedocs.io/en/latest/install.html
## Installation
@@ -48,14 +54,14 @@ JupyterHub can be installed with `pip` (and the proxy with `npm`) or `conda`:
```bash
python3 -m pip install jupyterhub
npm install -g configurable-http-proxy
python3 -m pip install notebook # needed if running the notebook servers locally
python3 -m pip install jupyterlab notebook # needed if running the notebook servers in the same environment
```
**conda** (one command installs jupyterhub and proxy):
```bash
conda install -c conda-forge jupyterhub # installs jupyterhub and proxy
conda install notebook # needed if running the notebook servers locally
conda install jupyterlab notebook # needed if running the notebook servers in the same environment
```
Test your installation. If installed, these commands should return the packages'
@@ -74,7 +80,7 @@ To start the Hub server, run the command:
jupyterhub
```
Visit `https://localhost:8000` in your browser, and sign in with your unix
Visit `http://localhost:8000` in your browser, and sign in with your unix
credentials.
To **allow multiple users to sign in** to the Hub server, you must start

View File

@@ -5,10 +5,12 @@ from pathlib import Path
from pytablewriter import MarkdownTableWriter
from ruamel.yaml import YAML
import jupyterhub
from jupyterhub.scopes import scope_definitions
HERE = os.path.abspath(os.path.dirname(__file__))
PARENT = Path(HERE).parent.parent.absolute()
DOCS = Path(HERE).parent.parent.absolute()
REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml")
class ScopeTableGenerator:
@@ -98,22 +100,26 @@ class ScopeTableGenerator:
def write_api(self):
"""Generates the API description in markdown format and writes it into `rest-api.yml`"""
filename = f"{PARENT}/rest-api.yml"
filename = REST_API_YAML
yaml = YAML(typ='rt')
yaml.preserve_quotes = True
scope_dict = {}
with open(filename, 'r+') as f:
with open(filename) as f:
content = yaml.load(f.read())
f.seek(0)
for scope in self.scopes:
description = self.scopes[scope]['description']
doc_description = self.scopes[scope].get('doc_description', '')
if doc_description:
description = doc_description
scope_dict[scope] = description
content['securityDefinitions']['oauth2']['scopes'] = scope_dict
content["info"]["version"] = jupyterhub.__version__
for scope in self.scopes:
description = self.scopes[scope]['description']
doc_description = self.scopes[scope].get('doc_description', '')
if doc_description:
description = doc_description
scope_dict[scope] = description
content['components']['securitySchemes']['oauth2']['flows'][
'authorizationCode'
]['scopes'] = scope_dict
with open(filename, 'w') as f:
yaml.dump(content, f)
f.truncate()
def main():

View File

@@ -123,13 +123,13 @@ has,
define the `server` role.
To restore the JupyterHub 1.x behavior of servers being able to do anything their owners can do,
use the scope `all`:
use the scope `inherit` (for 'inheriting' the owner's permissions):
```python
c.JupyterHub.load_roles = [
{
'name': 'server',
'scopes': ['all'],
'scopes': ['inherit'],
}
]
```

View File

@@ -0,0 +1,128 @@
(api-only)=
# Deploying JupyterHub in "API only mode"
As a service for deploying and managing Jupyter servers for users, JupyterHub
exposes this functionality _primarily_ via a [REST API](rest).
For convenience, JupyterHub also ships with a _basic_ web UI built using that REST API.
The basic web UI enables users to click a button to quickly start and stop their servers,
and it lets admins perform some basic user and server management tasks.
The REST API has always provided additional functionality beyond what is available in the basic web UI.
Similarly, we avoid implementing UI functionality that is also not available via the API.
With JupyterHub 2.0, the basic web UI will **always** be composed using the REST API.
In other words, no UI pages should rely on information not available via the REST API.
Previously, some admin UI functionality could only be achieved via admin pages,
such as paginated requests.
## Limited UI customization via templates
The JupyterHub UI is customizable via extensible HTML [templates](templates),
but this has some limited scope to what can be customized.
Adding some content and messages to existing pages is well supported,
but changing the page flow and what pages are available are beyond the scope of what is customizable.
## Rich UI customization with REST API based apps
Increasingly, JupyterHub is used purely as an API for managing Jupyter servers
for other Jupyter-based applications that might want to present a different user experience.
If you want a fully customized user experience,
you can now disable the Hub UI and use your own pages together with the JupyterHub REST API
to build your own web application to serve your users,
relying on the Hub only as an API for managing users and servers.
One example of such an application is [BinderHub][], which powers https://mybinder.org,
and motivates many of these changes.
BinderHub is distinct from a traditional JupyterHub deployment
because it uses temporary users created for each launch.
Instead of presenting a login page,
users are presented with a form to specify what environment they would like to launch:
![Binder launch form](../images/binderhub-form.png)
When a launch is requested:
1. an image is built, if necessary
2. a temporary user is created,
3. a server is launched for that user, and
4. when running, users are redirected to an already running server with an auth token in the URL
5. after the session is over, the user is deleted
This means that a lot of JupyterHub's UI flow doesn't make sense:
- there is no way for users to login
- the human user doesn't map onto a JupyterHub `User` in a meaningful way
- when a server isn't running, there isn't a 'restart your server' action available because the user has been deleted
- users do not have any access to any Hub functionality, so presenting pages for those features would be confusing
BinderHub is one of the motivating use cases for JupyterHub supporting being used _only_ via its API.
We'll use BinderHub here as an example of various configuration options.
[binderhub]: https://binderhub.readthedocs.io
## Disabling Hub UI
`c.JupyterHub.hub_routespec` is a configuration option to specify which URL prefix should be routed to the Hub.
The default is `/` which means that the Hub will receive all requests not already specified to be routed somewhere else.
There are three values that are most logical for `hub_routespec`:
- `/` - this is the default, and used in most deployments.
It is also the only option prior to JupyterHub 1.4.
- `/hub/` - this serves only Hub pages, both UI and API
- `/hub/api` - this serves _only the Hub API_, so all Hub UI is disabled,
aside from the OAuth confirmation page, if used.
If you choose a hub routespec other than `/`,
the main JupyterHub feature you will lose is the automatic handling of requests for `/user/:username`
when the requested server is not running.
JupyterHub's handling of this request shows this page,
telling you that the server is not running,
with a button to launch it again:
![screenshot of hub page for server not running](../images/server-not-running.png)
If you set `hub_routespec` to something other than `/`,
it is likely that you also want to register another destination for `/` to handle requests to not-running servers.
If you don't, you will see a default 404 page from the proxy:
![screenshot of CHP default 404](../images/chp-404.png)
For mybinder.org, the default "start my server" page doesn't make sense,
because when a server is gone, there is no restart action.
Instead, we provide hints about how to get back to a link to start a _new_ server:
![screenshot of mybinder.org 404](../images/binder-404.png)
To achieve this, mybinder.org registers a route for `/` that goes to a custom endpoint
that runs nginx and only serves this static HTML error page.
This is set with
```python
c.Proxy.extra_routes = {
"/": "http://custom-404-entpoint/",
}
```
You may want to use an alternate behavior, such as redirecting to a landing page,
or taking some other action based on the requested page.
If you use `c.JupyterHub.hub_routespec = "/hub/"`,
then all the Hub pages will be available,
and only this default-page-404 issue will come up.
If you use `c.JupyterHub.hub_routespec = "/hub/api/"`,
then only the Hub _API_ will be available,
and all UI will be up to you.
mybinder.org takes this last option,
because none of the Hub UI pages really make sense.
Binder users don't have any reason to know or care that JupyterHub happens
to be an implementation detail of how their environment is managed.
Seeing Hub error pages and messages in that situation is more likely to be confusing than helpful.
:::{versionadded} 1.4
`c.JupyterHub.hub_routespec` and `c.Proxy.extra_routes` are new in JupyterHub 1.4.
:::

View File

@@ -219,7 +219,7 @@ In case of the need to run the jupyterhub under /jhub/ or other location please
httpd.conf amendments:
```bash
RewriteRule /jhub/(.*) ws://127.0.0.1:8000/jhub/$1 [NE.P,L]
RewriteRule /jhub/(.*) ws://127.0.0.1:8000/jhub/$1 [NE,P,L]
RewriteRule /jhub/(.*) http://127.0.0.1:8000/jhub/$1 [NE,P,L]
ProxyPass /jhub/ http://127.0.0.1:8000/jhub/

View File

@@ -76,13 +76,26 @@ c.InteractiveShellApp.extensions.append("cython")
### Example: Enable a Jupyter notebook configuration setting for all users
:::{note}
These examples configure the Jupyter ServerApp,
which is used by JupyterLab, the default in JupyterHub 2.0.
If you are using the classing Jupyter Notebook server,
the same things should work,
with the following substitutions:
- Where you see `jupyter_server_config`, use `jupyter_notebook_config`
- Where you see `NotebookApp`, use `ServerApp`
:::
To enable Jupyter notebook's internal idle-shutdown behavior (requires
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_notebook_config.py`
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_server_config.py`
file:
```python
# shutdown the server after no activity for an hour
c.NotebookApp.shutdown_no_activity_timeout = 60 * 60
c.ServerApp.shutdown_no_activity_timeout = 60 * 60
# shutdown kernels after no activity for 20 minutes
c.MappingKernelManager.cull_idle_timeout = 20 * 60
# check for idle kernels every two minutes
@@ -112,8 +125,8 @@ Assuming I have a Python 2 and Python 3 environment that I want to make
sure are available, I can install their specs system-wide (in /usr/local) with:
```bash
/path/to/python3 -m IPython kernel install --prefix=/usr/local
/path/to/python2 -m IPython kernel install --prefix=/usr/local
/path/to/python3 -m ipykernel install --prefix=/usr/local
/path/to/python2 -m ipykernel install --prefix=/usr/local
```
## Multi-user hosts vs. Containers
@@ -176,12 +189,40 @@ The number of named servers per user can be limited by setting
c.JupyterHub.named_server_limit_per_user = 5
```
## Switching to Jupyter Server
(classic-notebook-ui)=
[Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/) is a new Tornado Server backend for Jupyter web applications (e.g. JupyterLab 3.0 uses this package as its default backend).
## Switching back to classic notebook
By default, the single-user notebook server uses the (old) `NotebookApp` from the [notebook](https://github.com/jupyter/notebook) package. You can switch to using Jupyter Server's `ServerApp` backend (this will likely become the default in future releases) by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable to:
By default the single-user server launches JupyterLab,
which is based on [Jupyter Server][].
This is the default server when running JupyterHub ≥ 2.0.
You can switch to using the legacy Jupyter Notebook server by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable
(in the single-user environment) to:
```bash
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
```
[jupyter server]: https://jupyter-server.readthedocs.io
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
:::{versionchanged} 2.0
JupyterLab is now the default singleuser UI, if available,
which is based on the [Jupyter Server][],
no longer the legacy [Jupyter Notebook][] server.
JupyterHub prior to 2.0 launched the legacy notebook server (`jupyter notebook`),
and Jupyter server could be selected by specifying
```python
# jupyterhub_config.py
c.Spawner.cmd = ["jupyter-labhub"]
```
or for an otherwise customized Jupyter Server app,
set the environment variable:
```bash
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
```
:::

View File

@@ -16,10 +16,12 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
proxy
separate-proxy
rest
rest-api
server-api
monitoring
database
templates
api-only
../events/index
config-user-env
config-examples

View File

@@ -0,0 +1,27 @@
# JupyterHub REST API
Below is an interactive view of JupyterHub's OpenAPI specification.
<!-- client-rendered openapi UI copied from FastAPI -->
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.1/swagger-ui-bundle.js"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<!-- render the ui here -->
<div id="openapi-ui"></div>
<script>
const ui = SwaggerUIBundle({
url: '../_static/rest-api.yml',
dom_id: '#openapi-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true,
});
</script>

View File

@@ -1,14 +0,0 @@
:orphan:
===================
JupyterHub REST API
===================
.. this doc exists as a resolvable link target
.. which _static files are not
.. meta::
:http-equiv=refresh: 0;url=../_static/rest-api/index.html
The rest API docs are `here <../_static/rest-api/index.html>`_
if you are not redirected automatically.

View File

@@ -1,3 +1,5 @@
(rest-api)=
# Using JupyterHub's REST API
This section will give you information on:
@@ -302,12 +304,8 @@ or kubernetes pods.
## Learn more about the API
You can see the full [JupyterHub REST API][] for details. This REST API Spec can
be viewed in a more [interactive style on swagger's petstore][].
Both resources contain the same information and differ only in its display.
Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
You can see the full [JupyterHub REST API][] for details.
[interactive style on swagger's petstore]: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#!/default
[openapi initiative]: https://www.openapis.org/
[jupyterhub rest api]: ./rest-api
[jupyter notebook rest api]: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/HEAD/notebook/services/api/api.yaml

45
docs/test_docs.py Normal file
View File

@@ -0,0 +1,45 @@
import sys
from pathlib import Path
from subprocess import run
from ruamel.yaml import YAML
yaml = YAML(typ="safe")
here = Path(__file__).absolute().parent
root = here.parent
def test_rest_api_version():
version_py = root.joinpath("jupyterhub", "_version.py")
rest_api_yaml = root.joinpath("docs", "source", "_static", "rest-api.yml")
ns = {}
with version_py.open() as f:
exec(f.read(), {}, ns)
jupyterhub_version = ns["__version__"]
with rest_api_yaml.open() as f:
rest_api = yaml.load(f)
rest_api_version = rest_api["info"]["version"]
assert jupyterhub_version == rest_api_version
def test_restapi_scopes():
run([sys.executable, "source/rbac/generate-scope-table.py"], cwd=here, check=True)
run(
['pre-commit', 'run', 'prettier', '--files', 'source/_static/rest-api.yml'],
cwd=here,
check=False,
)
run(
[
"git",
"diff",
"--no-pager",
"--exit-code",
str(here.joinpath("source", "_static", "rest-api.yml")),
],
cwd=here,
check=True,
)

View File

@@ -29,7 +29,7 @@ def get_token():
token_file = here.joinpath("service-token")
log.info(f"Loading token from {token_file}")
with token_file.open("r") as f:
token = f.read()
token = f.read().strip()
return token

View File

@@ -5457,9 +5457,9 @@ npm-run-path@^4.0.0:
path-key "^3.0.0"
nth-check@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==
dependencies:
boolbase "^1.0.0"
@@ -7278,9 +7278,9 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3:
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
to-fast-properties@^2.0.0:
version "2.0.0"

View File

@@ -1,14 +1,8 @@
"""JupyterHub version info"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
version_info = (
2,
0,
0,
"b1", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases
)
# version_info updated by running `tbump`
version_info = (2, 0, 0, "rc5", "")
# pep 440 version: no dot before beta/rc, but before .dev
# 0.1.0rc1
@@ -16,7 +10,9 @@ version_info = (
# 0.1.0b1.dev
# 0.1.0.dev
__version__ = ".".join(map(str, version_info[:3])) + ".".join(version_info[3:])
__version__ = ".".join(map(str, version_info[:3])) + ".".join(version_info[3:]).rstrip(
"."
)
# Singleton flag to only log the major/minor mismatch warning once per mismatch combo.
_version_mismatch_warning_logged = {}

View File

@@ -308,12 +308,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
"filter": "",
}
]
elif 'all' in raw_scopes:
raw_scopes = ['all']
elif 'inherit' in raw_scopes:
raw_scopes = ['inherit']
scope_descriptions = [
{
"scope": "all",
"description": scopes.scope_definitions['all']['description'],
"scope": "inherit",
"description": scopes.scope_definitions['inherit'][
'description'
],
"filter": "",
}
]

View File

@@ -31,6 +31,9 @@ class APIHandler(BaseHandler):
- methods for REST API models
"""
# accept token-based authentication for API requests
_accept_token_auth = True
@property
def content_security_policy(self):
return '; '.join([super().content_security_policy, "default-src 'none'"])
@@ -210,6 +213,7 @@ class APIHandler(BaseHandler):
'last_activity': isoformat(token.last_activity),
'expires_at': isoformat(token.expires_at),
'note': token.note,
'session_id': token.session_id,
'oauth_client': token.oauth_client.description
or token.oauth_client.identifier,
}

View File

@@ -129,7 +129,7 @@ class GroupAPIHandler(_GroupAPIHandler):
self.write(json.dumps(self.group_model(group)))
self.set_status(201)
@needs_scope('admin:groups')
@needs_scope('delete:groups')
def delete(self, group_name):
"""Delete a group by name"""
group = self.find_group(group_name)

View File

@@ -58,6 +58,14 @@ class SelfAPIHandler(APIHandler):
model = get_model(user)
# add session_id associated with token
# added in 2.0
token = self.get_token()
if token:
model["session_id"] = token.session_id
else:
model["session_id"] = None
# add scopes to identify model,
# but not the scopes we added to ensure we could read our own model
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
@@ -266,7 +274,7 @@ class UserAPIHandler(APIHandler):
self.write(json.dumps(self.user_model(user)))
self.set_status(201)
@needs_scope('admin:users')
@needs_scope('delete:users')
async def delete(self, user_name):
user = self.find_user(user_name)
if user is None:
@@ -397,9 +405,11 @@ class UserTokenListAPIHandler(APIHandler):
token_roles = body.get('roles')
try:
api_token = user.new_api_token(
note=note, expires_in=body.get('expires_in', None), roles=token_roles
note=note,
expires_in=body.get('expires_in', None),
roles=token_roles,
)
except NameError:
except KeyError:
raise web.HTTPError(404, "Requested roles %r not found" % token_roles)
except ValueError:
raise web.HTTPError(
@@ -421,6 +431,7 @@ class UserTokenListAPIHandler(APIHandler):
token_model = self.token_model(orm.APIToken.find(self.db, api_token))
token_model['token'] = api_token
self.write(json.dumps(token_model))
self.set_status(201)
class UserTokenAPIHandler(APIHandler):
@@ -483,6 +494,11 @@ class UserServerAPIHandler(APIHandler):
@needs_scope('servers')
async def post(self, user_name, server_name=''):
user = self.find_user(user_name)
if user is None:
# this can be reached if a token has `servers`
# permission on *all* users
raise web.HTTPError(404)
if server_name:
if not self.allow_named_servers:
raise web.HTTPError(400, "Named servers are not enabled.")
@@ -525,7 +541,7 @@ class UserServerAPIHandler(APIHandler):
self.set_header('Content-Type', 'text/plain')
self.set_status(status)
@needs_scope('servers')
@needs_scope('delete:servers')
async def delete(self, user_name, server_name=''):
user = self.find_user(user_name)
options = self.get_json_body()

View File

@@ -90,6 +90,7 @@ from .log import CoroutineLogFormatter, log_request
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command, EntryPointType, Callable
from .utils import (
AnyTimeoutError,
catch_db_error,
maybe_future,
url_path_join,
@@ -1518,6 +1519,25 @@ class JupyterHub(Application):
""",
).tag(config=True)
use_legacy_stopped_server_status_code = Bool(
False,
help="""
Return 503 rather than 424 when request comes in for a non-running server.
Prior to JupyterHub 2.0, we returned a 503 when any request came in for
a user server that was currently not running. By default, JupyterHub 2.0
will return a 424 - this makes operational metric dashboards more useful.
JupyterLab < 3.2 expected the 503 to know if the user server is no longer
running, and prompted the user to start their server. Set this config to
true to retain the old behavior, so JupyterLab < 3.2 can continue to show
the appropriate UI when the user server is stopped.
This option will be removed in a future release.
""",
config=True,
)
def init_handlers(self):
h = []
# load handlers from the authenticator
@@ -2050,7 +2070,7 @@ class JupyterHub(Application):
if role_spec['name'] == 'admin':
app_log.warning(
"Configuration specifies both admin_users and users in the admin role specification. "
"If admin role is present in config, c.authenticator.admin_users should not be used."
"If admin role is present in config, c.Authenticator.admin_users should not be used."
)
app_log.info(
"Merging admin_users set with users list in admin role"
@@ -2089,7 +2109,7 @@ class JupyterHub(Application):
)
Class = orm.get_class(kind)
orm_obj = Class.find(db, bname)
if orm_obj:
if orm_obj is not None:
orm_role_bearers.append(orm_obj)
else:
app_log.info(
@@ -2098,6 +2118,11 @@ class JupyterHub(Application):
if kind == 'users':
orm_obj = await self._get_or_create_user(bname)
orm_role_bearers.append(orm_obj)
elif kind == 'groups':
group = orm.Group(name=bname)
db.add(group)
db.commit()
orm_role_bearers.append(group)
else:
raise ValueError(
f"{kind} {bname} defined in config role definition {predef_role['name']} but not present in database"
@@ -2331,7 +2356,7 @@ class JupyterHub(Application):
continue
try:
await Server.from_orm(service.orm.server).wait_up(timeout=1, http=True)
except TimeoutError:
except AnyTimeoutError:
self.log.warning(
"Cannot connect to %s service %s at %s",
service.kind,
@@ -2409,7 +2434,7 @@ class JupyterHub(Application):
)
try:
await user._wait_up(spawner)
except TimeoutError:
except AnyTimeoutError:
self.log.error(
"%s does not appear to be running at %s, shutting it down.",
spawner._log_name,
@@ -2773,7 +2798,7 @@ class JupyterHub(Application):
await gen.with_timeout(
timedelta(seconds=max(init_spawners_timeout, 1)), init_spawners_future
)
except gen.TimeoutError:
except AnyTimeoutError:
self.log.warning(
"init_spawners did not complete within %i seconds. "
"Allowing to complete in the background.",
@@ -3036,7 +3061,7 @@ class JupyterHub(Application):
await Server.from_orm(service.orm.server).wait_up(
http=True, timeout=1, ssl_context=ssl_context
)
except TimeoutError:
except AnyTimeoutError:
if service.managed:
status = await service.spawner.poll()
if status is not None:

View File

@@ -1173,3 +1173,22 @@ class DummyAuthenticator(Authenticator):
return data['username']
return None
return data['username']
class NullAuthenticator(Authenticator):
"""Null Authenticator for JupyterHub
For cases where authentication should be disabled,
e.g. only allowing access via API tokens.
.. versionadded:: 2.0
"""
# auto_login skips 'Login with...' page on Hub 0.8
auto_login = True
# for Hub 0.7, show 'login with...'
login_service = 'null'
def get_handlers(self, app):
return []

View File

@@ -47,6 +47,7 @@ from ..metrics import TOTAL_USERS
from ..objects import Server
from ..spawner import LocalProcessSpawner
from ..user import User
from ..utils import AnyTimeoutError
from ..utils import get_accepted_mimetype
from ..utils import maybe_future
from ..utils import url_path_join
@@ -70,6 +71,12 @@ SESSION_COOKIE_NAME = 'jupyterhub-session-id'
class BaseHandler(RequestHandler):
"""Base Handler class with access to common methods and properties."""
# by default, only accept cookie-based authentication
# The APIHandler base class enables token auth
# versionadded: 2.0
_accept_cookie_auth = True
_accept_token_auth = False
async def prepare(self):
"""Identify the user during the prepare stage of each request
@@ -339,6 +346,7 @@ class BaseHandler(RequestHandler):
auth_info['auth_state'] = await user.get_auth_state()
return await self.auth_to_user(auth_info, user)
@functools.lru_cache()
def get_token(self):
"""get token from authorization header"""
token = self.get_auth_token()
@@ -409,9 +417,11 @@ class BaseHandler(RequestHandler):
async def get_current_user(self):
"""get current username"""
if not hasattr(self, '_jupyterhub_user'):
user = None
try:
user = self.get_current_user_token()
if user is None:
if self._accept_token_auth:
user = self.get_current_user_token()
if user is None and self._accept_cookie_auth:
user = self.get_current_user_cookie()
if user and isinstance(user, User):
user = await self.refresh_auth(user)
@@ -490,7 +500,7 @@ class BaseHandler(RequestHandler):
session_id = self.get_session_cookie()
if session_id:
# clear session id
self.clear_cookie(SESSION_COOKIE_NAME, **kwargs)
self.clear_cookie(SESSION_COOKIE_NAME, path=self.base_url, **kwargs)
if user:
# user is logged in, clear any tokens associated with the current session
@@ -569,7 +579,9 @@ class BaseHandler(RequestHandler):
so other services on this domain can read it.
"""
session_id = uuid.uuid4().hex
self._set_cookie(SESSION_COOKIE_NAME, session_id, encrypted=False)
self._set_cookie(
SESSION_COOKIE_NAME, session_id, encrypted=False, path=self.base_url
)
return session_id
def set_service_cookie(self, user):
@@ -1019,7 +1031,7 @@ class BaseHandler(RequestHandler):
await gen.with_timeout(
timedelta(seconds=self.slow_spawn_timeout), finish_spawn_future
)
except gen.TimeoutError:
except AnyTimeoutError:
# waiting_for_response indicates server process has started,
# but is yet to become responsive.
if spawner._spawn_pending and not spawner._waiting_for_response:
@@ -1166,7 +1178,7 @@ class BaseHandler(RequestHandler):
try:
await gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), future)
except gen.TimeoutError:
except AnyTimeoutError:
# hit timeout, but stop is still pending
self.log.warning(
"User %s:%s server is slow to stop (timeout=%s)",
@@ -1355,7 +1367,7 @@ class UserUrlHandler(BaseHandler):
**Changed Behavior as of 1.0** This handler no longer triggers a spawn. Instead, it checks if:
1. server is not active, serve page prompting for spawn (status: 503)
1. server is not active, serve page prompting for spawn (status: 424)
2. server is ready (This shouldn't happen! Proxy isn't updated yet. Wait a bit and redirect.)
3. server is active, redirect to /hub/spawn-pending to monitor launch progress
(will redirect back when finished)
@@ -1374,7 +1386,14 @@ class UserUrlHandler(BaseHandler):
self.log.warning(
"Failing suspected API request to not-running server: %s", self.request.path
)
self.set_status(503)
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# resource *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
self.set_header("Content-Type", "application/json")
spawn_url = urlparse(self.request.full_url())._replace(query="")
@@ -1539,15 +1558,17 @@ class UserUrlHandler(BaseHandler):
self.redirect(pending_url, status=303)
return
# if we got here, the server is not running
# serve a page prompting for spawn and 503 error
# visiting /user/:name no longer triggers implicit spawn
# without explicit user action
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# page *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
spawn_url = url_concat(
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
{"next": self.request.uri},
)
self.set_status(503)
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
auth_state = await user.get_auth_state()
html = await self.render_template(

View File

@@ -44,6 +44,7 @@ from . import utils
from .metrics import CHECK_ROUTES_DURATION_SECONDS
from .metrics import PROXY_POLL_DURATION_SECONDS
from .objects import Server
from .utils import AnyTimeoutError
from .utils import exponential_backoff
from .utils import url_path_join
from jupyterhub.traitlets import Command
@@ -718,7 +719,7 @@ class ConfigurableHTTPProxy(Proxy):
_check_process()
try:
await server.wait_up(1)
except TimeoutError:
except AnyTimeoutError:
continue
else:
break

View File

@@ -57,7 +57,7 @@ def get_default_roles():
{
'name': 'token',
'description': 'Token with same permissions as its owner',
'scopes': ['all'],
'scopes': ['inherit'],
},
]
return default_roles
@@ -89,6 +89,7 @@ def expand_self_scope(name):
'users:activity',
'read:users:activity',
'servers',
'delete:servers',
'read:servers',
'tokens',
'read:tokens',
@@ -213,7 +214,7 @@ def _check_scopes(*args, rolename=None):
or
scopes (list): list of scopes to check
Raises NameError if scope does not exist
Raises KeyError if scope does not exist
"""
allowed_scopes = set(scopes.scope_definitions.keys())
@@ -227,11 +228,13 @@ def _check_scopes(*args, rolename=None):
for scope in args:
scopename, _, filter_ = scope.partition('!')
if scopename not in allowed_scopes:
raise NameError(f"Scope '{scope}' {log_role} does not exist")
if scopename == "all":
raise KeyError("Draft scope 'all' is now called 'inherit'")
raise KeyError(f"Scope '{scope}' {log_role} does not exist")
if filter_:
full_filter = f"!{filter_}"
if not any(f in scope for f in allowed_filters):
raise NameError(
raise KeyError(
f"Scope filter '{full_filter}' in scope '{scope}' {log_role} does not exist"
)
@@ -321,7 +324,7 @@ def delete_role(db, rolename):
db.commit()
app_log.info('Role %s has been deleted', rolename)
else:
raise NameError('Cannot remove role %r that does not exist', rolename)
raise KeyError('Cannot remove role %r that does not exist', rolename)
def existing_only(func):
@@ -412,7 +415,7 @@ def _token_allowed_role(db, token, role):
expanded_scopes = _get_subscopes(role, owner=owner)
implicit_permissions = {'all', 'read:all'}
implicit_permissions = {'inherit', 'read:inherit'}
explicit_scopes = expanded_scopes - implicit_permissions
# ignore horizontal filters
no_filter_scopes = {
@@ -431,37 +434,40 @@ def _token_allowed_role(db, token, role):
return True
else:
app_log.warning(
f"Token requesting scopes exceeding owner {owner.name}: {disallowed_scopes}"
f"Token requesting role {role.name} with scopes not held by owner {owner.name}: {disallowed_scopes}"
)
return False
def assign_default_roles(db, entity):
"""Assigns default role to an entity:
"""Assigns default role(s) to an entity:
users and services get 'user' role, or admin role if they have admin flag
tokens get 'token' role
"""
if isinstance(entity, orm.Group):
pass
elif isinstance(entity, orm.APIToken):
app_log.debug('Assigning default roles to tokens')
app_log.debug('Assigning default role to token')
default_token_role = orm.Role.find(db, 'token')
if not entity.roles and (entity.user or entity.service) is not None:
default_token_role.tokens.append(entity)
app_log.info('Added role %s to token %s', default_token_role.name, entity)
db.commit()
db.commit()
# users and services can have 'user' or 'admin' roles as default
else:
kind = type(entity).__name__
app_log.debug(f'Assigning default roles to {kind} {entity.name}')
app_log.debug(f'Assigning default role to {kind} {entity.name}')
_switch_default_role(db, entity, entity.admin)
def update_roles(db, entity, roles):
"""Updates object's roles checking for requested permissions
if object is orm.APIToken
"""Add roles to an entity (token, user, etc.)
If it is an API token, check role permissions against token owner
prior to assignment to avoid permission expansion.
Otherwise, it just calls `grant_role` for each role.
"""
standard_permissions = {'all', 'read:all'}
for rolename in roles:
if isinstance(entity, orm.APIToken):
role = orm.Role.find(db, rolename)
@@ -474,12 +480,11 @@ def update_roles(db, entity, roles):
app_log.info('Adding role %s to token: %s', role.name, entity)
else:
raise ValueError(
f'Requested token role {rolename} of {entity} has more permissions than the token owner'
f'Requested token role {rolename} for {entity} has more permissions than the token owner'
)
else:
raise NameError('Role %r does not exist' % rolename)
raise KeyError(f'Role {rolename} does not exist')
else:
app_log.debug('Assigning default roles to %s', type(entity).__name__)
grant_role(db, entity=entity, rolename=rolename)

View File

@@ -30,19 +30,22 @@ scope_definitions = {
'description': 'Your own resources',
'doc_description': 'The users own resources _(metascope for users, resolves to (no_scope) for services)_',
},
'all': {
'inherit': {
'description': 'Anything you have access to',
'doc_description': 'Everything that the token-owning entity can access _(metascope for tokens)_',
},
'admin:users': {
'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.',
'subscopes': ['admin:auth_state', 'users', 'read:roles:users'],
'subscopes': ['admin:auth_state', 'users', 'read:roles:users', 'delete:users'],
},
'admin:auth_state': {'description': 'Read a users authentication state.'},
'users': {
'description': 'Read and write permissions to user models (excluding servers, tokens and authentication state).',
'subscopes': ['read:users', 'list:users', 'users:activity'],
},
'delete:users': {
'description': "Delete users.",
},
'list:users': {
'description': 'List users, including at least their names.',
'subscopes': ['read:users:name'],
@@ -76,12 +79,13 @@ scope_definitions = {
'admin:server_state': {'description': 'Read and write users server state.'},
'servers': {
'description': 'Start and stop user servers.',
'subscopes': ['read:servers'],
'subscopes': ['read:servers', 'delete:servers'],
},
'read:servers': {
'description': 'Read users names and their server models (excluding the server state).',
'subscopes': ['read:users:name'],
},
'delete:servers': {'description': "Stop and delete users' servers."},
'tokens': {
'description': 'Read, write, create and delete user tokens.',
'subscopes': ['read:tokens'],
@@ -89,7 +93,7 @@ scope_definitions = {
'read:tokens': {'description': 'Read user tokens.'},
'admin:groups': {
'description': 'Read and write group information, create and delete groups.',
'subscopes': ['groups', 'read:roles:groups'],
'subscopes': ['groups', 'read:roles:groups', 'delete:groups'],
},
'groups': {
'description': 'Read and write group information, including adding/removing users to/from groups.',
@@ -104,6 +108,9 @@ scope_definitions = {
'subscopes': ['read:groups:name'],
},
'read:groups:name': {'description': 'Read group names.'},
'delete:groups': {
'description': "Delete groups.",
},
'list:services': {
'description': 'List services, including at least their names.',
'subscopes': ['read:services:name'],
@@ -288,7 +295,7 @@ def get_scopes_for(orm_object):
)
if isinstance(orm_object, orm.APIToken):
app_log.warning(f"Authenticated with token {orm_object}")
app_log.debug(f"Authenticated with token {orm_object}")
owner = orm_object.user or orm_object.service
token_scopes = roles.expand_roles_to_scopes(orm_object)
if orm_object.client_id != "jupyterhub":
@@ -310,13 +317,13 @@ def get_scopes_for(orm_object):
owner_scopes = roles.expand_roles_to_scopes(owner)
if token_scopes == {'all'}:
# token_scopes is only 'all', return owner scopes as-is
if token_scopes == {'inherit'}:
# token_scopes is only 'inherit', return scopes inherited from owner as-is
# short-circuit common case where we don't need to compute an intersection
return owner_scopes
if 'all' in token_scopes:
token_scopes.remove('all')
if 'inherit' in token_scopes:
token_scopes.remove('inherit')
token_scopes |= owner_scopes
intersection = _intersect_expanded_scopes(

View File

@@ -1023,8 +1023,8 @@ class HubAuthenticated:
self._hub_auth_user_cache = None
raise
# store tokens passed via url or header in a cookie for future requests
url_token = self.hub_auth.get_token(self)
# store ?token=... tokens passed via url in a cookie for future requests
url_token = self.get_argument('token', '')
if (
user_model
and url_token

View File

@@ -1,7 +1,12 @@
"""Make a single-user app based on the environment:
- $JUPYTERHUB_SINGLEUSER_APP, the base Application class, to be wrapped in JupyterHub authentication.
default: notebook.notebookapp.NotebookApp
default: jupyter_server.serverapp.ServerApp
.. versionchanged:: 2.0
Default app changed to launch `jupyter labhub`.
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
"""
import os
@@ -9,12 +14,55 @@ from traitlets import import_item
from .mixins import make_singleuser_app
JUPYTERHUB_SINGLEUSER_APP = (
os.environ.get("JUPYTERHUB_SINGLEUSER_APP") or "notebook.notebookapp.NotebookApp"
)
JUPYTERHUB_SINGLEUSER_APP = os.environ.get("JUPYTERHUB_SINGLEUSER_APP")
if JUPYTERHUB_SINGLEUSER_APP:
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
else:
App = None
_import_error = None
for JUPYTERHUB_SINGLEUSER_APP in (
"jupyter_server.serverapp.ServerApp",
"notebook.notebookapp.NotebookApp",
):
try:
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
except ImportError as e:
continue
if _import_error is None:
_import_error = e
else:
break
if App is None:
raise _import_error
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
SingleUserNotebookApp = make_singleuser_app(App)
main = SingleUserNotebookApp.launch_instance
def main():
"""Launch a jupyterhub single-user server"""
if not os.environ.get("JUPYTERHUB_SINGLEUSER_APP"):
# app not specified, launch jupyter-labhub by default,
# if jupyterlab is recent enough (3.1).
# This is a minimally extended ServerApp that does:
# 1. ensure lab extension is enabled, and
# 2. set default URL to `/lab`
import re
_version_pat = re.compile(r"(\d+)\.(\d+)")
try:
import jupyterlab
from jupyterlab.labhubapp import SingleUserLabApp
m = _version_pat.match(jupyterlab.__version__)
except Exception:
m = None
if m is not None:
version_tuple = tuple(int(v) for v in m.groups())
if version_tuple >= (3, 1):
return SingleUserLabApp.launch_instance()
return SingleUserNotebookApp.launch_instance()

View File

@@ -18,6 +18,7 @@ import sys
import warnings
from datetime import datetime
from datetime import timezone
from importlib import import_module
from textwrap import dedent
from urllib.parse import urlparse
@@ -606,10 +607,34 @@ class SingleUserNotebookAppMixin(Configurable):
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
await asyncio.sleep(t)
def _log_app_versions(self):
"""Log application versions at startup
Logs versions of jupyterhub and singleuser-server base versions (jupyterlab, jupyter_server, notebook)
"""
self.log.info(f"Starting jupyterhub single-user server version {__version__}")
# don't log these package versions
seen = {"jupyterhub", "traitlets", "jupyter_core", "builtins"}
for cls in self.__class__.mro():
module_name = cls.__module__.partition(".")[0]
if module_name not in seen:
seen.add(module_name)
try:
mod = import_module(module_name)
mod_version = getattr(mod, "__version__")
except Exception:
mod_version = ""
self.log.info(
f"Extending {cls.__module__}.{cls.__name__} from {module_name} {mod_version}"
)
def initialize(self, argv=None):
# disable trash by default
# this can be re-enabled by config
self.config.FileContentsManager.delete_to_trash = False
self._log_app_versions()
return super().initialize(argv)
def start(self):
@@ -715,6 +740,18 @@ class SingleUserNotebookAppMixin(Configurable):
orig_loader = env.loader
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
def load_server_extensions(self):
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect
r = super().load_server_extensions()
# clear the token in PageConfig at this step
# so that cookie auth is used
# FIXME: in the future,
# it would probably make sense to set page_config.token to the token
# from the current request.
if 'page_config_data' in self.web_app.settings:
self.web_app.settings['page_config_data']['token'] = ''
return r
def detect_base_package(App):
"""Detect the base package for an App class

View File

@@ -15,8 +15,6 @@ from subprocess import Popen
from tempfile import mkdtemp
from urllib.parse import urlparse
if os.name == 'nt':
import psutil
from async_generator import aclosing
from sqlalchemy import inspect
from tornado.ioloop import PeriodicCallback
@@ -38,12 +36,14 @@ from .objects import Server
from .traitlets import ByteSpecification
from .traitlets import Callable
from .traitlets import Command
from .utils import AnyTimeoutError
from .utils import exponential_backoff
from .utils import maybe_future
from .utils import random_port
from .utils import url_path_join
# FIXME: remove when we drop Python 3.5 support
if os.name == 'nt':
import psutil
def _quote_safe(s):
@@ -1263,7 +1263,7 @@ class Spawner(LoggingConfigurable):
timeout=timeout,
)
return r
except TimeoutError:
except AnyTimeoutError:
return False

View File

@@ -972,6 +972,11 @@ async def test_bad_spawn(app, bad_spawn):
assert app.users.count_active_users()['pending'] == 0
async def test_spawn_nosuch_user(app):
r = await api_request(app, 'users', "nosuchuser", 'server', method='post')
assert r.status_code == 404
async def test_slow_bad_spawn(app, no_patience, slow_bad_spawn):
db = app.db
name = 'zaphod'
@@ -1366,8 +1371,8 @@ async def test_get_new_token_deprecated(app, headers, status):
@mark.parametrize(
"headers, status, note, expires_in",
[
({}, 200, 'test note', None),
({}, 200, '', 100),
({}, 201, 'test note', None),
({}, 201, '', 100),
({'Authorization': 'token bad'}, 403, '', None),
],
)
@@ -1386,7 +1391,7 @@ async def test_get_new_token(app, headers, status, note, expires_in):
app, 'users/admin/tokens', method='post', headers=headers, data=body
)
assert r.status_code == status
if status != 200:
if status != 201:
return
# check the new-token reply
reply = r.json()
@@ -1424,10 +1429,10 @@ async def test_get_new_token(app, headers, status, note, expires_in):
@mark.parametrize(
"as_user, for_user, status",
[
('admin', 'other', 200),
('admin', 'other', 201),
('admin', 'missing', 403),
('user', 'other', 403),
('user', 'user', 200),
('user', 'user', 201),
],
)
async def test_token_for_user(app, as_user, for_user, status):
@@ -1448,7 +1453,7 @@ async def test_token_for_user(app, as_user, for_user, status):
)
assert r.status_code == status
reply = r.json()
if status != 200:
if status != 201:
return
assert 'token' in reply
@@ -1486,7 +1491,7 @@ async def test_token_authenticator_noauth(app):
data=json.dumps(data) if data else None,
noauth=True,
)
assert r.status_code == 200
assert r.status_code == 201
reply = r.json()
assert 'token' in reply
r = await api_request(app, 'authorizations', 'token', reply['token'])
@@ -1509,7 +1514,7 @@ async def test_token_authenticator_dict_noauth(app):
data=json.dumps(data) if data else None,
noauth=True,
)
assert r.status_code == 200
assert r.status_code == 201
reply = r.json()
assert 'token' in reply
r = await api_request(app, 'authorizations', 'token', reply['token'])

View File

@@ -3,6 +3,7 @@
# Distributed under the terms of the Modified BSD License.
import logging
from unittest import mock
from urllib.parse import urlparse
import pytest
from requests import HTTPError
@@ -12,6 +13,8 @@ from .mocking import MockPAMAuthenticator
from .mocking import MockStructGroup
from .mocking import MockStructPasswd
from .utils import add_user
from .utils import async_requests
from .utils import public_url
from jupyterhub import auth
from jupyterhub import crypto
from jupyterhub import orm
@@ -515,3 +518,12 @@ def test_deprecated_methods_subclass():
assert authenticator.check_whitelist("subclass-allowed")
assert not authenticator.check_allowed("otheruser")
assert not authenticator.check_whitelist("otheruser")
async def test_nullauthenticator(app):
with mock.patch.dict(
app.tornado_settings, {"authenticator": auth.NullAuthenticator(parent=app)}
):
r = await async_requests.get(public_url(app))
assert urlparse(r.url).path.endswith("/hub/login")
assert r.status_code == 403

View File

@@ -1,16 +1,13 @@
"""Tests for jupyterhub internal_ssl connections"""
import sys
import time
from subprocess import check_output
from unittest import mock
from urllib.parse import urlparse
import pytest
from requests.exceptions import ConnectionError
from requests.exceptions import SSLError
from tornado import gen
import jupyterhub
from ..utils import AnyTimeoutError
from .test_api import add_user
from .utils import async_requests
@@ -35,7 +32,7 @@ async def wait_for_spawner(spawner, timeout=10):
assert status is None
try:
await wait()
except TimeoutError:
except AnyTimeoutError:
continue
else:
break

View File

@@ -56,8 +56,8 @@ async def test_root_redirect(app):
r = await get_page(url, app, cookies=cookies)
path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name)
# serve "server not running" page, which has status 503
assert r.status_code == 503
# serve "server not running" page, which has status 424
assert r.status_code == 424
async def test_root_default_url_noauth(app):
@@ -172,7 +172,7 @@ async def test_spawn_redirect(app):
r = await get_page('user/' + name, app, hub=False, cookies=cookies)
path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'hub/user/%s/' % name)
assert r.status_code == 503
assert r.status_code == 424
async def test_spawn_handler_access(app):
@@ -507,13 +507,13 @@ async def test_user_redirect_deprecated(app, username):
print(urlparse(r.url))
path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'hub/user/%s/' % name)
assert r.status_code == 503
assert r.status_code == 424
r = await get_page('/user/baduser/test.ipynb', app, cookies=cookies, hub=False)
print(urlparse(r.url))
path = urlparse(r.url).path
assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name)
assert r.status_code == 503
assert r.status_code == 424
r = await get_page('/user/baduser/test.ipynb', app, hub=False)
r.raise_for_status()
@@ -578,6 +578,41 @@ async def test_login_page(app, url, params, redirected_url, form_action):
assert action.endswith(form_action)
@pytest.mark.parametrize(
"url, token_in",
[
("/home", "url"),
("/home", "header"),
("/login", "url"),
("/login", "header"),
],
)
async def test_page_with_token(app, user, url, token_in):
cookies = await app.login_user(user.name)
token = user.new_api_token()
if token_in == "url":
url = url_concat(url, {"token": token})
headers = None
elif token_in == "header":
headers = {
"Authorization": f"token {token}",
}
# request a page with ?token= in URL shouldn't be allowed
r = await get_page(
url,
app,
headers=headers,
allow_redirects=False,
)
if "/hub/login" in r.url:
assert r.status_code == 200
else:
assert r.status_code == 302
assert r.headers["location"].partition("?")[0].endswith("/hub/login")
assert not r.cookies
async def test_login_fail(app):
name = 'wash'
base_url = public_url(app)
@@ -1061,13 +1096,20 @@ async def test_token_page(app):
async def test_server_not_running_api_request(app):
cookies = await app.login_user("bees")
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 503
assert r.status_code == 424
assert r.headers["content-type"] == "application/json"
message = r.json()['message']
assert ujoin(app.base_url, "hub/spawn/bees") in message
assert " /user/bees" in message
async def test_server_not_running_api_request_legacy_status(app):
app.use_legacy_stopped_server_status_code = True
cookies = await app.login_user("bees")
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 503
async def test_metrics_no_auth(app):
r = await get_page("metrics", app)
assert r.status_code == 403

View File

@@ -28,7 +28,7 @@ def test_orm_roles(db):
user_role = orm.Role(name='user', scopes=['self'])
db.add(user_role)
if not token_role:
token_role = orm.Role(name='token', scopes=['all'])
token_role = orm.Role(name='token', scopes=['inherit'])
db.add(token_role)
if not service_role:
service_role = orm.Role(name='service', scopes=[])
@@ -182,6 +182,7 @@ def test_orm_roles_delete_cascade(db):
'admin:users',
'admin:auth_state',
'users',
'delete:users',
'list:users',
'read:users',
'users:activity',
@@ -218,6 +219,7 @@ def test_orm_roles_delete_cascade(db):
{
'admin:groups',
'groups',
'delete:groups',
'list:groups',
'read:groups',
'read:roles:groups',
@@ -229,6 +231,7 @@ def test_orm_roles_delete_cascade(db):
{
'admin:groups',
'groups',
'delete:groups',
'list:groups',
'read:groups',
'read:roles:groups',
@@ -366,7 +369,7 @@ async def test_creating_roles(app, role, role_def, response_type, response):
'info',
app_log.info('Role user scopes attribute has been changed'),
),
('non-existing', 'test-role2', 'error', NameError),
('non-existing', 'test-role2', 'error', KeyError),
('default', 'user', 'error', ValueError),
],
)
@@ -407,9 +410,9 @@ async def test_delete_roles(db, role_type, rolename, response_type, response):
},
'existing',
),
({'name': 'test-scopes-2', 'scopes': ['uses']}, NameError),
({'name': 'test-scopes-3', 'scopes': ['users:activities']}, NameError),
({'name': 'test-scopes-4', 'scopes': ['groups!goup=class-A']}, NameError),
({'name': 'test-scopes-2', 'scopes': ['uses']}, KeyError),
({'name': 'test-scopes-3', 'scopes': ['users:activities']}, KeyError),
({'name': 'test-scopes-4', 'scopes': ['groups!goup=class-A']}, KeyError),
],
)
async def test_scope_existence(tmpdir, request, role, response):
@@ -428,7 +431,7 @@ async def test_scope_existence(tmpdir, request, role, response):
assert added_role is not None
assert added_role.scopes == role['scopes']
elif response == NameError:
elif response == KeyError:
with pytest.raises(response):
roles.create_role(db, role)
added_role = orm.Role.find(db, role['name'])
@@ -575,7 +578,7 @@ async def test_load_roles_groups(tmpdir, request):
'name': 'head',
'description': 'Whole user access',
'scopes': ['users', 'admin:users'],
'groups': ['group3'],
'groups': ['group3', "group4"],
},
]
kwargs = {'load_groups': groups_to_load, 'load_roles': roles_to_load}
@@ -595,11 +598,13 @@ async def test_load_roles_groups(tmpdir, request):
group1 = orm.Group.find(db, name='group1')
group2 = orm.Group.find(db, name='group2')
group3 = orm.Group.find(db, name='group3')
group4 = orm.Group.find(db, name='group4')
# test group roles
assert group1.roles == []
assert group2 in assist_role.groups
assert group3 in head_role.groups
assert group4 in head_role.groups
# delete the test roles
for role in roles_to_load:
@@ -658,11 +663,11 @@ async def test_load_roles_user_tokens(tmpdir, request):
"headers, rolename, scopes, status",
[
# no role requested - gets default 'token' role
({}, None, None, 200),
({}, None, None, 201),
# role scopes within the user's default 'user' role
({}, 'self-reader', ['read:users'], 200),
({}, 'self-reader', ['read:users'], 201),
# role scopes outside of the user's role but within the group's role scopes of which the user is a member
({}, 'groups-reader', ['read:groups'], 200),
({}, 'groups-reader', ['read:groups'], 201),
# non-existing role request
({}, 'non-existing', [], 404),
# role scopes outside of both user's role and group's role scopes
@@ -1327,3 +1332,19 @@ async def test_token_keep_roles_on_restart():
for token in user.api_tokens:
hub.db.delete(token)
hub.db.commit()
async def test_login_default_role(app, username):
cookies = await app.login_user(username)
user = app.users[username]
# assert login new user gets 'user' role
assert [role.name for role in user.roles] == ["user"]
# clear roles, keep user
user.roles = []
app.db.commit()
# login *again*; user exists, shouldn't trigger change in roles
cookies = await app.login_user(username)
user = app.users[username]
assert user.roles == []

View File

@@ -477,7 +477,7 @@ async def test_metascope_all_expansion(app, create_user_with_scopes):
user = create_user_with_scopes('self')
user.new_api_token()
token = user.api_tokens[0]
# Check 'all' expansion
# Check 'inherit' expansion
token_scope_set = get_scopes_for(token)
user_scope_set = get_scopes_for(user)
assert user_scope_set == token_scope_set

View File

@@ -1,6 +1,10 @@
"""Tests for jupyterhub.singleuser"""
import os
import sys
from contextlib import contextmanager
from subprocess import CalledProcessError
from subprocess import check_output
from unittest import mock
from urllib.parse import urlparse
import pytest
@@ -14,6 +18,12 @@ from .utils import async_requests
from .utils import AsyncSession
@contextmanager
def nullcontext():
"""Python 3.7+ contextlib.nullcontext, backport for 3.6"""
yield
@pytest.mark.parametrize(
"access_scopes, server_name, expect_success",
[
@@ -171,3 +181,47 @@ def test_version():
[sys.executable, '-m', 'jupyterhub.singleuser', '--version']
).decode('utf8', 'replace')
assert jupyterhub.__version__ in out
@pytest.mark.parametrize(
"JUPYTERHUB_SINGLEUSER_APP",
[
"",
"notebook.notebookapp.NotebookApp",
"jupyter_server.serverapp.ServerApp",
],
)
def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
try:
import jupyter_server # noqa
except ImportError:
have_server = False
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
else:
have_server = True
expect_error = False
if expect_error:
ctx = pytest.raises(CalledProcessError)
else:
ctx = nullcontext()
with mock.patch.dict(
os.environ,
{
"JUPYTERHUB_SINGLEUSER_APP": JUPYTERHUB_SINGLEUSER_APP,
},
):
with ctx:
out = check_output(
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
).decode('utf8', 'replace')
if expect_error:
return
# use help-all output to check inheritance
if 'NotebookApp' in JUPYTERHUB_SINGLEUSER_APP or not have_server:
assert '--NotebookApp.' in out
assert '--ServerApp.' not in out
else:
assert '--ServerApp.' in out
assert '--NotebookApp.' not in out

View File

@@ -21,6 +21,7 @@ from ..objects import Server
from ..spawner import LocalProcessSpawner
from ..spawner import Spawner
from ..user import User
from ..utils import AnyTimeoutError
from ..utils import new_token
from ..utils import url_path_join
from .mocking import public_url
@@ -95,7 +96,7 @@ async def wait_for_spawner(spawner, timeout=10):
assert status is None
try:
await wait()
except TimeoutError:
except AnyTimeoutError:
continue
else:
break

View File

@@ -26,11 +26,41 @@ from .metrics import RUNNING_SERVERS
from .metrics import TOTAL_USERS
from .objects import Server
from .spawner import LocalProcessSpawner
from .utils import AnyTimeoutError
from .utils import make_ssl_context
from .utils import maybe_future
from .utils import url_path_join
# detailed messages about the most common failure-to-start errors,
# which manifest timeouts during start
start_timeout_message = """
Common causes of this timeout, and debugging tips:
1. Everything is working, but it took too long.
To fix: increase `Spawner.start_timeout` configuration
to a number of seconds that is enough for spawners to finish starting.
2. The server didn't finish starting,
or it crashed due to a configuration issue.
Check the single-user server's logs for hints at what needs fixing.
"""
http_timeout_message = """
Common causes of this timeout, and debugging tips:
1. The server didn't finish starting,
or it crashed due to a configuration issue.
Check the single-user server's logs for hints at what needs fixing.
2. The server started, but is not accessible at the specified URL.
This may be a configuration issue specific to your chosen Spawner.
Check the single-user server logs and resource to make sure the URL
is correct and accessible from the Hub.
3. (unlikely) Everything is working, but the server took too long to respond.
To fix: increase `Spawner.http_timeout` configuration
to a number of seconds that is enough for servers to become responsive.
"""
class UserDict(dict):
"""Like defaultdict, but for users
@@ -84,7 +114,7 @@ class UserDict(dict):
if user.name == key:
key = user.id
break
return dict.__contains__(self, key)
return super().__contains__(key)
def __getitem__(self, key):
"""UserDict allows retrieval of user by any of:
@@ -108,7 +138,7 @@ class UserDict(dict):
if orm_user.id not in self:
user = self[orm_user.id] = User(orm_user, self.settings)
return user
user = dict.__getitem__(self, orm_user.id)
user = super().__getitem__(orm_user.id)
user.db = self.db
return user
elif isinstance(key, int):
@@ -119,7 +149,7 @@ class UserDict(dict):
raise KeyError("No such user: %s" % id)
user = self.add(orm_user)
else:
user = dict.__getitem__(self, id)
user = super().__getitem__(id)
return user
else:
raise KeyError(repr(key))
@@ -145,7 +175,7 @@ class UserDict(dict):
self.db.expunge(orm_spawner)
if user.orm_user in self.db:
self.db.expunge(user.orm_user)
dict.__delitem__(self, user.id)
super().__delitem__(user.id)
def delete(self, key):
"""Delete a user from the cache and the database"""
@@ -707,11 +737,11 @@ class User:
db.commit()
except Exception as e:
if isinstance(e, gen.TimeoutError):
if isinstance(e, AnyTimeoutError):
self.log.warning(
"{user}'s server failed to start in {s} seconds, giving up".format(
user=self.name, s=spawner.start_timeout
)
f"{self.name}'s server failed to start"
f" in {spawner.start_timeout} seconds, giving up."
f"\n{start_timeout_message}"
)
e.reason = 'timeout'
self.settings['statsd'].incr('spawner.failure.timeout')
@@ -764,14 +794,11 @@ class User:
http=True, timeout=spawner.http_timeout, ssl_context=ssl_context
)
except Exception as e:
if isinstance(e, TimeoutError):
if isinstance(e, AnyTimeoutError):
self.log.warning(
"{user}'s server never showed up at {url} "
"after {http_timeout} seconds. Giving up".format(
user=self.name,
url=server.url,
http_timeout=spawner.http_timeout,
)
f"{self.name}'s server never showed up at {server.url}"
f" after {spawner.http_timeout} seconds. Giving up."
f"\n{http_timeout_message}"
)
e.reason = 'timeout'
self.settings['statsd'].incr('spawner.failure.http_timeout')

View File

@@ -23,12 +23,12 @@ from operator import itemgetter
from async_generator import aclosing
from sqlalchemy.exc import SQLAlchemyError
from tornado import gen
from tornado import ioloop
from tornado import web
from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPError
from tornado.log import app_log
from tornado.platform.asyncio import to_asyncio_future
# For compatibility with python versions 3.6 or earlier.
# asyncio.Task.all_tasks() is fully moved to asyncio.all_tasks() starting with 3.9. Also applies to current_task.
@@ -97,6 +97,10 @@ def make_ssl_context(keyfile, certfile, cafile=None, verify=True, check_hostname
return ssl_context
# AnyTimeoutError catches TimeoutErrors coming from asyncio, tornado, stdlib
AnyTimeoutError = (gen.TimeoutError, asyncio.TimeoutError, TimeoutError)
async def exponential_backoff(
pass_func,
fail_message,
@@ -182,7 +186,7 @@ async def exponential_backoff(
if dt < max_wait:
scale *= scale_factor
await asyncio.sleep(dt)
raise TimeoutError(fail_message)
raise asyncio.TimeoutError(fail_message)
async def wait_for_server(ip, port, timeout=10):
@@ -288,6 +292,31 @@ def authenticated_403(self):
raise web.HTTPError(403)
def admin_only(f):
"""Deprecated!"""
# write it this way to trigger deprecation warning at decoration time,
# not on the method call
warnings.warn(
"""@jupyterhub.utils.admin_only is deprecated in JupyterHub 2.0.
Use the new `@jupyterhub.scopes.needs_scope` decorator to resolve permissions,
or check against `self.current_user.parsed_scopes`.
""",
DeprecationWarning,
stacklevel=2,
)
# the original decorator
@auth_decorator
def admin_only(self):
"""Decorator for restricting access to admin users"""
user = self.current_user
if user is None or not user.admin:
raise web.HTTPError(403)
return admin_only(f)
@auth_decorator
def metrics_authentication(self):
"""Decorator for restricting access to metrics"""

View File

@@ -5,3 +5,41 @@ target_version = [
"py37",
"py38",
]
[tool.tbump]
# Uncomment this if your project is hosted on GitHub:
github_url = "https://github.com/jupyterhub/jupyterhub"
[tool.tbump.version]
current = "2.0.0rc5"
# Example of a semver regexp.
# Make sure this matches current_version before
# using tbump
regex = '''
(?P<major>\d+)
\.
(?P<minor>\d+)
\.
(?P<patch>\d+)
(?P<pre>((a|b|rc)\d+)|)
\.?
(?P<dev>(?<=\.)dev\d*|)
'''
[tool.tbump.git]
message_template = "Bump to {new_version}"
tag_template = "{new_version}"
# For each file to patch, add a [[tool.tbump.file]] config
# section containing the path of the file, relative to the
# pyproject.toml location.
[[tool.tbump.file]]
src = "jupyterhub/_version.py"
version_template = '({major}, {minor}, {patch}, "{pre}", "{dev}")'
search = "version_info = {current_version}"
[[tool.tbump.file]]
src = "docs/source/_static/rest-api.yml"
search = "version: {current_version}"

View File

@@ -46,10 +46,9 @@ def get_data_files():
"""Get data files in share/jupyter"""
data_files = []
ntrim = len(here + os.path.sep)
for (d, dirs, filenames) in os.walk(share_jupyterhub):
data_files.append((d[ntrim:], [pjoin(d, f) for f in filenames]))
rel_d = os.path.relpath(d, here)
data_files.append((rel_d, [os.path.join(rel_d, f) for f in filenames]))
return data_files
@@ -100,6 +99,7 @@ setup_args = dict(
'default = jupyterhub.auth:PAMAuthenticator',
'pam = jupyterhub.auth:PAMAuthenticator',
'dummy = jupyterhub.auth:DummyAuthenticator',
'null = jupyterhub.auth:NullAuthenticator',
],
'jupyterhub.proxies': [
'default = jupyterhub.proxy:ConfigurableHTTPProxy',
@@ -301,7 +301,7 @@ class develop_js_css(develop):
if not self.uninstall:
self.distribution.run_command('js')
self.distribution.run_command('css')
develop.run(self)
super().run()
setup_args['cmdclass']['develop'] = develop_js_css

View File

@@ -6,7 +6,6 @@ FROM $BASE_IMAGE
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
ADD install_jupyterhub /tmp/install_jupyterhub
ARG JUPYTERHUB_VERSION=main
# install pinned jupyterhub and ensure notebook is installed
RUN python3 /tmp/install_jupyterhub && \
python3 -m pip install notebook
ARG JUPYTERHUB_VERSION=git:HEAD
# install pinned jupyterhub
RUN python3 /tmp/install_jupyterhub

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -ex
docker build --build-arg JUPYTERHUB_VERSION=$DOCKER_TAG -t $DOCKER_REPO:$DOCKER_TAG .

View File

@@ -1,21 +0,0 @@
#!/bin/bash
set -ex
function get_hub_version() {
rm -f hub_version
V=$1
docker run --rm -v $PWD:/version -u $(id -u) -i $DOCKER_REPO:$DOCKER_TAG sh -c 'jupyterhub --version > /version/hub_version'
hub_xyz=$(cat hub_version)
split=( ${hub_xyz//./ } )
hub_xy="${split[0]}.${split[1]}"
# add .dev on hub_xy so it's 1.0.dev
if [[ ! -z "${split[3]:-}" ]]; then
hub_xy="${hub_xy}.${split[3]}"
fi
}
# tag e.g. 0.9 with main
get_hub_version
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
docker push $DOCKER_REPO:$hub_xy
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xyz
docker push $DOCKER_REPO:$hub_xyz

View File

@@ -3,19 +3,22 @@ import os
from subprocess import check_call
import sys
V = os.environ['JUPYTERHUB_VERSION']
version = os.environ['JUPYTERHUB_VERSION']
pip_install = [
sys.executable, '-m', 'pip', 'install', '--no-cache', '--upgrade',
'--upgrade-strategy', 'only-if-needed',
sys.executable,
'-m',
'pip',
'install',
'--no-cache',
'--upgrade',
'--upgrade-strategy',
'only-if-needed',
]
if V in {'main', 'HEAD'}:
req = 'https://github.com/jupyterhub/jupyterhub/archive/HEAD.tar.gz'
if version.startswith("git:"):
ref = version.partition(":")[-1]
req = f"https://github.com/jupyterhub/jupyterhub/archive/{ref}.tar.gz"
else:
version_info = [ int(part) for part in V.split('.') ]
version_info[-1] += 1
upper_bound = '.'.join(map(str, version_info))
vs = '>=%s,<%s' % (V, upper_bound)
req = 'jupyterhub%s' % vs
req = f"jupyterhub=={version}"
check_call(pip_install + [req])