mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +00:00
Compare commits
79 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bda14b487a | ||
![]() |
fd5cf8c360 | ||
![]() |
03758e5b46 | ||
![]() |
e540d143bb | ||
![]() |
b2c5ad40c5 | ||
![]() |
edfdf672d8 | ||
![]() |
39f19aef49 | ||
![]() |
8813bb63d4 | ||
![]() |
7c18d6fe14 | ||
![]() |
d1fe17d3cb | ||
![]() |
b8965c2017 | ||
![]() |
733d7bc158 | ||
![]() |
3caf3cfda8 | ||
![]() |
d076c55cca | ||
![]() |
3e185022c8 | ||
![]() |
857ee2885f | ||
![]() |
cd8dd56213 | ||
![]() |
f06902aa8f | ||
![]() |
bb109c6f75 | ||
![]() |
e525ec7b5b | ||
![]() |
356b98e19f | ||
![]() |
8c803e7a53 | ||
![]() |
2e21a6f4e0 | ||
![]() |
cfd31b14e3 | ||
![]() |
f03a620424 | ||
![]() |
440ad77ad5 | ||
![]() |
68835e97a2 | ||
![]() |
ce80c9c9cf | ||
![]() |
3c299fbfb7 | ||
![]() |
597f8ea6eb | ||
![]() |
d1181085bf | ||
![]() |
913832da48 | ||
![]() |
42f57f4a72 | ||
![]() |
d01a518c41 | ||
![]() |
65ce06b116 | ||
![]() |
468aa5e93c | ||
![]() |
5c01370e6f | ||
![]() |
21d08883a8 | ||
![]() |
59de506f20 | ||
![]() |
b34120ed81 | ||
![]() |
617978179d | ||
![]() |
0985d6fdf2 | ||
![]() |
2049fb0491 | ||
![]() |
a58fc6534b | ||
![]() |
a14f97b7aa | ||
![]() |
0a4cd5b4f2 | ||
![]() |
dca6d372df | ||
![]() |
3898c72921 | ||
![]() |
b25517efe8 | ||
![]() |
392dffd11e | ||
![]() |
510f6ea7e6 | ||
![]() |
296a0ad2f2 | ||
![]() |
487c4524ad | ||
![]() |
b2f0208fcc | ||
![]() |
84b9c3848c | ||
![]() |
9adbafdfb3 | ||
![]() |
9cf2b5101e | ||
![]() |
725fa3a48a | ||
![]() |
534dda3dc7 | ||
![]() |
b0c7df04ac | ||
![]() |
61b0e8bef5 | ||
![]() |
64f3938528 | ||
![]() |
85bc92d88e | ||
![]() |
7bcda18564 | ||
![]() |
86da36857e | ||
![]() |
530833e930 | ||
![]() |
3b0850fa9b | ||
![]() |
1366911be6 | ||
![]() |
fe276eac64 | ||
![]() |
9209ccd0de | ||
![]() |
3b2a1a37f9 | ||
![]() |
6007ba78b0 | ||
![]() |
9cb19cc342 | ||
![]() |
0f471f4e12 | ||
![]() |
68db740998 | ||
![]() |
9c0c6f25b7 | ||
![]() |
5f0077cb5b | ||
![]() |
3610454a12 | ||
![]() |
abc4bbebe4 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -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:"
|
||||
|
31
.github/workflows/support-bot.yml
vendored
Normal file
31
.github/workflows/support-bot.yml
vendored
Normal 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"
|
24
.github/workflows/test.yml
vendored
24
.github/workflows/test.yml
vendored
@@ -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
|
||||
@@ -182,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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.28.0
|
||||
rev: v2.29.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
@@ -10,7 +10,7 @@ repos:
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.9b0
|
||||
rev: 21.10b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
@@ -18,7 +18,7 @@ repos:
|
||||
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
|
||||
|
@@ -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
|
@@ -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
50
RELEASE.md
Normal 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
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you’ve found a security vulnerability in a Jupyter
|
||||
project, please report it to security@ipython.org. If you prefer to
|
||||
encrypt your security reports, you can use [this PGP public key](https://jupyter-notebook.readthedocs.io/en/stable/_downloads/1d303a645f2505a8fd283826fafc9908/ipython_security.asc).
|
@@ -14,6 +14,7 @@ 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
|
||||
|
@@ -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."
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
1196
docs/rest-api.yml
1196
docs/rest-api.yml
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,9 @@
|
||||
.navbar-brand {
|
||||
height: 4rem !important;
|
||||
}
|
||||
|
||||
/* hide redundant funky-formatted swagger-ui version */
|
||||
|
||||
.swagger-ui .info .title small {
|
||||
display: none !important;
|
||||
}
|
||||
|
1421
docs/source/_static/rest-api.yml
Normal file
1421
docs/source/_static/rest-api.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 -------------------------------------------------------
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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():
|
||||
|
@@ -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'],
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@@ -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/
|
||||
|
@@ -16,6 +16,7 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
|
||||
proxy
|
||||
separate-proxy
|
||||
rest
|
||||
rest-api
|
||||
server-api
|
||||
monitoring
|
||||
database
|
||||
|
27
docs/source/reference/rest-api.md
Normal file
27
docs/source/reference/rest-api.md
Normal 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>
|
@@ -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.
|
@@ -302,12 +302,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
45
docs/test_docs.py
Normal 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,
|
||||
)
|
@@ -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,
|
||||
"b2", # 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, "rc4", "")
|
||||
|
||||
# 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 = {}
|
||||
|
@@ -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": "",
|
||||
}
|
||||
]
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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))
|
||||
@@ -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.")
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
@@ -1021,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:
|
||||
@@ -1168,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)",
|
||||
@@ -1357,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)
|
||||
@@ -1376,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="")
|
||||
@@ -1541,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(
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
@@ -214,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())
|
||||
@@ -228,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"
|
||||
)
|
||||
|
||||
@@ -322,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):
|
||||
@@ -413,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 = {
|
||||
@@ -432,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)
|
||||
@@ -475,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)
|
||||
|
||||
|
||||
|
@@ -30,7 +30,7 @@ scope_definitions = {
|
||||
'description': 'Your own resources',
|
||||
'doc_description': 'The user’s 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)_',
|
||||
},
|
||||
@@ -295,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":
|
||||
@@ -317,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(
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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'])
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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=[])
|
||||
@@ -369,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),
|
||||
],
|
||||
)
|
||||
@@ -410,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):
|
||||
@@ -431,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'])
|
||||
@@ -578,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}
|
||||
@@ -598,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:
|
||||
@@ -661,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
|
||||
@@ -1330,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 == []
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
@@ -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"""
|
||||
|
@@ -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.0rc4"
|
||||
|
||||
# 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}"
|
||||
|
5
setup.py
5
setup.py
@@ -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
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user