mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f5dc005a70 | ||
![]() |
5fd8f0f596 | ||
![]() |
26ceafa8a3 | ||
![]() |
2e2ed8a4ff | ||
![]() |
6cc734f884 | ||
![]() |
4f7f07d3b7 | ||
![]() |
d436c97e3d | ||
![]() |
807c5b8ff9 | ||
![]() |
8da06d1259 | ||
![]() |
1c1be8a24b | ||
![]() |
897606b00c | ||
![]() |
615af5eb33 | ||
![]() |
85f94c12fc | ||
![]() |
ccfee4d235 | ||
![]() |
a2ba55756d | ||
![]() |
1b3e94db6c | ||
![]() |
614d9d89d0 | ||
![]() |
05a3f5aa9a | ||
![]() |
4f47153123 | ||
![]() |
a14d9ecaa1 | ||
![]() |
6815f30d36 | ||
![]() |
13172e6856 | ||
![]() |
ebc9fd7758 | ||
![]() |
0761a5db02 | ||
![]() |
46e7a231fe | ||
![]() |
ffa5a20e2f | ||
![]() |
2088a57ffe | ||
![]() |
345805781f | ||
![]() |
9eb52ea788 | ||
![]() |
fb1405ecd8 |
55
.github/workflows/release.yml
vendored
55
.github/workflows/release.yml
vendored
@@ -1,15 +1,32 @@
|
|||||||
# Build releases and (on tags) publish to PyPI
|
# This is a GitHub workflow defining a set of jobs with a set of steps.
|
||||||
|
# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
|
||||||
|
#
|
||||||
|
# Test build release artifacts (PyPI package, Docker images) and publish them on
|
||||||
|
# pushed git tags.
|
||||||
|
#
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
# always build releases (to make sure wheel-building works)
|
|
||||||
# but only publish to PyPI on tags
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "!dependabot/**"
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- "**.rst"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/release.yml"
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- "**.rst"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/release.yml"
|
||||||
|
branches-ignore:
|
||||||
|
- "dependabot/**"
|
||||||
|
- "pre-commit-ci-update-config"
|
||||||
|
tags:
|
||||||
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-release:
|
build-release:
|
||||||
@@ -96,7 +113,6 @@ jobs:
|
|||||||
# Setup docker to build for multiple platforms, see:
|
# Setup docker to build for multiple platforms, see:
|
||||||
# https://github.com/docker/build-push-action/tree/v2.4.0#usage
|
# https://github.com/docker/build-push-action/tree/v2.4.0#usage
|
||||||
# https://github.com/docker/build-push-action/blob/v2.4.0/docs/advanced/multi-platform.md
|
# https://github.com/docker/build-push-action/blob/v2.4.0/docs/advanced/multi-platform.md
|
||||||
|
|
||||||
- name: Set up QEMU (for docker buildx)
|
- name: Set up QEMU (for docker buildx)
|
||||||
uses: docker/setup-qemu-action@25f0500ff22e406f7191a2a8ba8cda16901ca018 # associated tag: v1.0.2
|
uses: docker/setup-qemu-action@25f0500ff22e406f7191a2a8ba8cda16901ca018 # associated tag: v1.0.2
|
||||||
|
|
||||||
@@ -120,6 +136,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" -p "${{ secrets.DOCKERHUB_TOKEN }}"
|
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" -p "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||||
|
|
||||||
|
# image: jupyterhub/jupyterhub
|
||||||
|
#
|
||||||
# https://github.com/jupyterhub/action-major-minor-tag-calculator
|
# https://github.com/jupyterhub/action-major-minor-tag-calculator
|
||||||
# If this is a tagged build this will return additional parent tags.
|
# If this is a tagged build this will return additional parent tags.
|
||||||
# E.g. 1.2.3 is expanded to Docker tags
|
# E.g. 1.2.3 is expanded to Docker tags
|
||||||
@@ -137,7 +155,7 @@ jobs:
|
|||||||
branchRegex: ^\w[\w-.]*$
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
- name: Build and push jupyterhub
|
- name: Build and push jupyterhub
|
||||||
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -146,8 +164,8 @@ jobs:
|
|||||||
# array into a comma separated list of tags
|
# array into a comma separated list of tags
|
||||||
tags: ${{ join(fromJson(steps.jupyterhubtags.outputs.tags)) }}
|
tags: ${{ join(fromJson(steps.jupyterhubtags.outputs.tags)) }}
|
||||||
|
|
||||||
# jupyterhub-onbuild
|
# image: jupyterhub/jupyterhub-onbuild
|
||||||
|
#
|
||||||
- name: Get list of jupyterhub-onbuild tags
|
- name: Get list of jupyterhub-onbuild tags
|
||||||
id: onbuildtags
|
id: onbuildtags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
@@ -158,7 +176,7 @@ jobs:
|
|||||||
branchRegex: ^\w[\w-.]*$
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
- name: Build and push jupyterhub-onbuild
|
- name: Build and push jupyterhub-onbuild
|
||||||
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }}
|
BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }}
|
||||||
@@ -167,8 +185,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ join(fromJson(steps.onbuildtags.outputs.tags)) }}
|
tags: ${{ join(fromJson(steps.onbuildtags.outputs.tags)) }}
|
||||||
|
|
||||||
# jupyterhub-demo
|
# image: jupyterhub/jupyterhub-demo
|
||||||
|
#
|
||||||
- name: Get list of jupyterhub-demo tags
|
- name: Get list of jupyterhub-demo tags
|
||||||
id: demotags
|
id: demotags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
@@ -179,7 +197,7 @@ jobs:
|
|||||||
branchRegex: ^\w[\w-.]*$
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
- name: Build and push jupyterhub-demo
|
- name: Build and push jupyterhub-demo
|
||||||
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }}
|
BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }}
|
||||||
@@ -191,7 +209,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
||||||
|
|
||||||
# jupyterhub/singleuser
|
# image: jupyterhub/singleuser
|
||||||
|
#
|
||||||
- name: Get list of jupyterhub/singleuser tags
|
- name: Get list of jupyterhub/singleuser tags
|
||||||
id: singleusertags
|
id: singleusertags
|
||||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||||
@@ -202,7 +221,7 @@ jobs:
|
|||||||
branchRegex: ^\w[\w-.]*$
|
branchRegex: ^\w[\w-.]*$
|
||||||
|
|
||||||
- name: Build and push jupyterhub/singleuser
|
- name: Build and push jupyterhub/singleuser
|
||||||
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
|
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
|
||||||
|
64
.github/workflows/test-docs.yml
vendored
Normal file
64
.github/workflows/test-docs.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# This is a GitHub workflow defining a set of jobs with a set of steps.
|
||||||
|
# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
|
||||||
|
#
|
||||||
|
# This workflow validates the REST API definition and runs the pytest tests in
|
||||||
|
# the docs/ folder. This workflow does not build the documentation. That is
|
||||||
|
# instead tested via ReadTheDocs (https://readthedocs.org/projects/jupyterhub/).
|
||||||
|
#
|
||||||
|
name: Test docs
|
||||||
|
|
||||||
|
# The tests defined in docs/ are currently influenced by changes to _version.py
|
||||||
|
# and scopes.py.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
- "jupyterhub/_version.py"
|
||||||
|
- "jupyterhub/scopes.py"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/test-docs.yml"
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
- "jupyterhub/_version.py"
|
||||||
|
- "jupyterhub/scopes.py"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/test-docs.yml"
|
||||||
|
branches-ignore:
|
||||||
|
- "dependabot/**"
|
||||||
|
- "pre-commit-ci-update-config"
|
||||||
|
tags:
|
||||||
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# UTF-8 content may be interpreted as ascii and causes errors without this.
|
||||||
|
LANG: C.UTF-8
|
||||||
|
PYTEST_ADDOPTS: "--verbose --color=yes"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-rest-api-definition:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate REST API definition
|
||||||
|
uses: char0n/swagger-editor-validate@182d1a5d26ff5c2f4f452c43bd55e2c7d8064003
|
||||||
|
with:
|
||||||
|
definition-file: docs/source/_static/rest-api.yml
|
||||||
|
|
||||||
|
test-docs:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
|
||||||
|
- name: Install requirements
|
||||||
|
run: |
|
||||||
|
pip install -r docs/requirements.txt pytest -e .
|
||||||
|
|
||||||
|
- name: pytest docs/
|
||||||
|
run: |
|
||||||
|
pytest docs/
|
41
.github/workflows/test.yml
vendored
41
.github/workflows/test.yml
vendored
@@ -1,14 +1,28 @@
|
|||||||
# This is a GitHub workflow defining a set of jobs with a set of steps.
|
# This is a GitHub workflow defining a set of jobs with a set of steps.
|
||||||
# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
|
# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
|
||||||
#
|
#
|
||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
# Trigger the workflow's on all PRs but only on pushed tags or commits to
|
|
||||||
# main/master branch to avoid PRs developed in a GitHub fork's dedicated branch
|
|
||||||
# to trigger.
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- "**.rst"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/test.yml"
|
||||||
push:
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
- "**.md"
|
||||||
|
- "**.rst"
|
||||||
|
- ".github/workflows/*"
|
||||||
|
- "!.github/workflows/test.yml"
|
||||||
|
branches-ignore:
|
||||||
|
- "dependabot/**"
|
||||||
|
- "pre-commit-ci-update-config"
|
||||||
|
tags:
|
||||||
|
- "**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -17,25 +31,6 @@ env:
|
|||||||
PYTEST_ADDOPTS: "--verbose --color=yes"
|
PYTEST_ADDOPTS: "--verbose --color=yes"
|
||||||
|
|
||||||
jobs:
|
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/
|
|
||||||
|
|
||||||
jstest:
|
jstest:
|
||||||
# Run javascript tests
|
# Run javascript tests
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.29.1
|
rev: v2.31.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args:
|
args:
|
||||||
@@ -22,7 +22,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.0.1
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
|
@@ -117,8 +117,7 @@ To start the Hub server, run the command:
|
|||||||
|
|
||||||
jupyterhub
|
jupyterhub
|
||||||
|
|
||||||
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
Visit `http://localhost:8000` in your browser, and sign in with your system username and password.
|
||||||
PAM credentials.
|
|
||||||
|
|
||||||
_Note_: To allow multiple users to sign in to the server, you will need to
|
_Note_: To allow multiple users to sign in to the server, you will need to
|
||||||
run the `jupyterhub` command as a _privileged user_, such as root.
|
run the `jupyterhub` command as a _privileged user_, such as root.
|
||||||
|
@@ -3,8 +3,10 @@
|
|||||||
alabaster_jupyterhub
|
alabaster_jupyterhub
|
||||||
autodoc-traits
|
autodoc-traits
|
||||||
myst-parser
|
myst-parser
|
||||||
|
pre-commit
|
||||||
pydata-sphinx-theme
|
pydata-sphinx-theme
|
||||||
pytablewriter>=0.56
|
pytablewriter>=0.56
|
||||||
|
ruamel.yaml
|
||||||
sphinx>=1.7
|
sphinx>=1.7
|
||||||
sphinx-copybutton
|
sphinx-copybutton
|
||||||
sphinx-jsonschema
|
sphinx-jsonschema
|
||||||
|
@@ -6,7 +6,7 @@ info:
|
|||||||
description: The REST API for JupyterHub
|
description: The REST API for JupyterHub
|
||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
version: 2.0.1
|
version: 2.0.2
|
||||||
servers:
|
servers:
|
||||||
- url: /hub/api
|
- url: /hub/api
|
||||||
security:
|
security:
|
||||||
|
@@ -8,6 +8,36 @@ command line for details.
|
|||||||
|
|
||||||
## 2.0
|
## 2.0
|
||||||
|
|
||||||
|
### [2.0.2] 2022-01-10
|
||||||
|
|
||||||
|
2.0.2 fixes a regression in 2.0.1 causing false positives
|
||||||
|
rejecting valid requests as cross-origin,
|
||||||
|
mostly when JupyterHub is behind additional proxies.
|
||||||
|
|
||||||
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/2.0.1...2.0.2))
|
||||||
|
|
||||||
|
#### Bugs fixed
|
||||||
|
|
||||||
|
- use outermost proxied entry when looking up browser protocol [#3757](https://github.com/jupyterhub/jupyterhub/pull/3757) ([@minrk](https://github.com/minrk))
|
||||||
|
|
||||||
|
#### Maintenance and upkeep improvements
|
||||||
|
|
||||||
|
- remove unused macro with missing references [#3760](https://github.com/jupyterhub/jupyterhub/pull/3760) ([@minrk](https://github.com/minrk))
|
||||||
|
- ci: refactor to avoid triggering all tests on changes to docs [#3750](https://github.com/jupyterhub/jupyterhub/pull/3750) ([@consideRatio](https://github.com/consideRatio))
|
||||||
|
- Extra test_cors_check tests [#3746](https://github.com/jupyterhub/jupyterhub/pull/3746) ([@manics](https://github.com/manics))
|
||||||
|
|
||||||
|
#### Documentation improvements
|
||||||
|
|
||||||
|
- DOCS: Update theme configuration [#3754](https://github.com/jupyterhub/jupyterhub/pull/3754) ([@choldgraf](https://github.com/choldgraf))
|
||||||
|
- DOC: Add note about allowed_users not being set [#3748](https://github.com/jupyterhub/jupyterhub/pull/3748) ([@choldgraf](https://github.com/choldgraf))
|
||||||
|
- localhost URL is http, not https [#3747](https://github.com/jupyterhub/jupyterhub/pull/3747) ([@minrk](https://github.com/minrk))
|
||||||
|
|
||||||
|
#### Contributors to this release
|
||||||
|
|
||||||
|
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2021-12-22&to=2022-01-10&type=c))
|
||||||
|
|
||||||
|
[@choldgraf](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2021-12-22..2022-01-10&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-12-22..2022-01-10&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agithub-actions+updated%3A2021-12-22..2022-01-10&type=Issues) | [@jakob-keller](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajakob-keller+updated%3A2021-12-22..2022-01-10&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2021-12-22..2022-01-10&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2021-12-22..2022-01-10&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-12-22..2022-01-10&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apre-commit-ci+updated%3A2021-12-22..2022-01-10&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Awelcome+updated%3A2021-12-22..2022-01-10&type=Issues)
|
||||||
|
|
||||||
### [2.0.1]
|
### [2.0.1]
|
||||||
|
|
||||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/2.0.0...2.0.1))
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/2.0.0...2.0.1))
|
||||||
@@ -1359,7 +1389,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
|||||||
|
|
||||||
First preview release
|
First preview release
|
||||||
|
|
||||||
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/2.0.1...HEAD
|
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/2.0.2...HEAD
|
||||||
|
[2.0.2]: https://github.com/jupyterhub/jupyterhub/compare/2.0.1...2.0.2
|
||||||
[2.0.1]: https://github.com/jupyterhub/jupyterhub/compare/2.0.0...2.0.1
|
[2.0.1]: https://github.com/jupyterhub/jupyterhub/compare/2.0.0...2.0.1
|
||||||
[2.0.0]: https://github.com/jupyterhub/jupyterhub/compare/1.5.0...2.0.0
|
[2.0.0]: https://github.com/jupyterhub/jupyterhub/compare/1.5.0...2.0.0
|
||||||
[1.5.0]: https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0
|
[1.5.0]: https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0
|
||||||
|
@@ -130,6 +130,23 @@ html_static_path = ['_static']
|
|||||||
|
|
||||||
htmlhelp_basename = 'JupyterHubdoc'
|
htmlhelp_basename = 'JupyterHubdoc'
|
||||||
|
|
||||||
|
html_theme_options = {
|
||||||
|
"icon_links": [
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"url": "https://github.com/jupyterhub/jupyterhub",
|
||||||
|
"icon": "fab fa-github-square",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Discourse",
|
||||||
|
"url": "https://discourse.jupyter.org/c/jupyterhub/10",
|
||||||
|
"icon": "fab fa-discourse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"use_edit_page_button": True,
|
||||||
|
"navbar_align": "left",
|
||||||
|
}
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
latex_elements = {
|
latex_elements = {
|
||||||
|
@@ -16,6 +16,10 @@ c.Authenticator.allowed_users = {'mal', 'zoe', 'inara', 'kaylee'}
|
|||||||
Users in the `allowed_users` set are added to the Hub database when the Hub is
|
Users in the `allowed_users` set are added to the Hub database when the Hub is
|
||||||
started.
|
started.
|
||||||
|
|
||||||
|
```{warning}
|
||||||
|
If this configuration value is not set, then **all authenticated users will be allowed into your hub**.
|
||||||
|
```
|
||||||
|
|
||||||
## Configure admins (`admin_users`)
|
## Configure admins (`admin_users`)
|
||||||
|
|
||||||
```{note}
|
```{note}
|
||||||
|
@@ -1,16 +1,33 @@
|
|||||||
|
"""
|
||||||
|
This script updates two files with the RBAC scope descriptions found in
|
||||||
|
`scopes.py`.
|
||||||
|
|
||||||
|
The files are:
|
||||||
|
|
||||||
|
1. scope-table.md
|
||||||
|
|
||||||
|
This file is git ignored and referenced by the documentation.
|
||||||
|
|
||||||
|
2. rest-api.yml
|
||||||
|
|
||||||
|
This file is JupyterHub's REST API schema. Both a version and the RBAC
|
||||||
|
scopes descriptions are updated in it.
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from subprocess import run
|
||||||
|
|
||||||
from pytablewriter import MarkdownTableWriter
|
from pytablewriter import MarkdownTableWriter
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
import jupyterhub
|
from jupyterhub import __version__
|
||||||
from jupyterhub.scopes import scope_definitions
|
from jupyterhub.scopes import scope_definitions
|
||||||
|
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
DOCS = Path(HERE).parent.parent.absolute()
|
DOCS = Path(HERE).parent.parent.absolute()
|
||||||
REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml")
|
REST_API_YAML = DOCS.joinpath("source", "_static", "rest-api.yml")
|
||||||
|
SCOPE_TABLE_MD = Path(HERE).joinpath("scope-table.md")
|
||||||
|
|
||||||
|
|
||||||
class ScopeTableGenerator:
|
class ScopeTableGenerator:
|
||||||
@@ -82,8 +99,9 @@ class ScopeTableGenerator:
|
|||||||
return table_rows
|
return table_rows
|
||||||
|
|
||||||
def write_table(self):
|
def write_table(self):
|
||||||
"""Generates the scope table in markdown format and writes it into `scope-table.md`"""
|
"""Generates the RBAC scopes reference documentation as a markdown table
|
||||||
filename = f"{HERE}/scope-table.md"
|
and writes it to the .gitignored `scope-table.md`."""
|
||||||
|
filename = SCOPE_TABLE_MD
|
||||||
table_name = ""
|
table_name = ""
|
||||||
headers = ["Scope", "Grants permission to:"]
|
headers = ["Scope", "Grants permission to:"]
|
||||||
values = self._parse_scopes()
|
values = self._parse_scopes()
|
||||||
@@ -99,15 +117,20 @@ class ScopeTableGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def write_api(self):
|
def write_api(self):
|
||||||
"""Generates the API description in markdown format and writes it into `rest-api.yml`"""
|
"""Loads `rest-api.yml` and writes it back with a dynamically set
|
||||||
|
JupyterHub version field and list of RBAC scopes descriptions from
|
||||||
|
`scopes.py`."""
|
||||||
filename = REST_API_YAML
|
filename = REST_API_YAML
|
||||||
yaml = YAML(typ='rt')
|
|
||||||
|
yaml = YAML(typ="rt")
|
||||||
yaml.preserve_quotes = True
|
yaml.preserve_quotes = True
|
||||||
|
yaml.indent(mapping=2, offset=2, sequence=4)
|
||||||
|
|
||||||
scope_dict = {}
|
scope_dict = {}
|
||||||
with open(filename) as f:
|
with open(filename) as f:
|
||||||
content = yaml.load(f.read())
|
content = yaml.load(f.read())
|
||||||
|
|
||||||
content["info"]["version"] = jupyterhub.__version__
|
content["info"]["version"] = __version__
|
||||||
for scope in self.scopes:
|
for scope in self.scopes:
|
||||||
description = self.scopes[scope]['description']
|
description = self.scopes[scope]['description']
|
||||||
doc_description = self.scopes[scope].get('doc_description', '')
|
doc_description = self.scopes[scope].get('doc_description', '')
|
||||||
@@ -121,6 +144,12 @@ class ScopeTableGenerator:
|
|||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
yaml.dump(content, f)
|
yaml.dump(content, f)
|
||||||
|
|
||||||
|
run(
|
||||||
|
['pre-commit', 'run', 'prettier', '--files', filename],
|
||||||
|
cwd=HERE,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
table_generator = ScopeTableGenerator()
|
table_generator = ScopeTableGenerator()
|
||||||
|
@@ -10,7 +10,9 @@ here = Path(__file__).absolute().parent
|
|||||||
root = here.parent
|
root = here.parent
|
||||||
|
|
||||||
|
|
||||||
def test_rest_api_version():
|
def test_rest_api_version_is_updated():
|
||||||
|
"""Checks that the version in JupyterHub's REST API definition file
|
||||||
|
(rest-api.yml) is matching the JupyterHub version."""
|
||||||
version_py = root.joinpath("jupyterhub", "_version.py")
|
version_py = root.joinpath("jupyterhub", "_version.py")
|
||||||
rest_api_yaml = root.joinpath("docs", "source", "_static", "rest-api.yml")
|
rest_api_yaml = root.joinpath("docs", "source", "_static", "rest-api.yml")
|
||||||
ns = {}
|
ns = {}
|
||||||
@@ -25,18 +27,17 @@ def test_rest_api_version():
|
|||||||
assert jupyterhub_version == rest_api_version
|
assert jupyterhub_version == rest_api_version
|
||||||
|
|
||||||
|
|
||||||
def test_restapi_scopes():
|
def test_rest_api_rbac_scope_descriptions_are_updated():
|
||||||
|
"""Checks that the RBAC scope descriptions in JupyterHub's REST API
|
||||||
|
definition file (rest-api.yml) as can be updated by generate-scope-table.py
|
||||||
|
matches what is committed."""
|
||||||
run([sys.executable, "source/rbac/generate-scope-table.py"], cwd=here, check=True)
|
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(
|
run(
|
||||||
[
|
[
|
||||||
"git",
|
"git",
|
||||||
"diff",
|
|
||||||
"--no-pager",
|
"--no-pager",
|
||||||
|
"diff",
|
||||||
|
"--color=always",
|
||||||
"--exit-code",
|
"--exit-code",
|
||||||
str(here.joinpath("source", "_static", "rest-api.yml")),
|
str(here.joinpath("source", "_static", "rest-api.yml")),
|
||||||
],
|
],
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
# version_info updated by running `tbump`
|
# version_info updated by running `tbump`
|
||||||
version_info = (2, 0, 1, "", "")
|
version_info = (2, 0, 2, "", "")
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
# 0.1.0rc1
|
# 0.1.0rc1
|
||||||
|
@@ -16,6 +16,7 @@ from tornado import web
|
|||||||
from .. import orm
|
from .. import orm
|
||||||
from .. import roles
|
from .. import roles
|
||||||
from .. import scopes
|
from .. import scopes
|
||||||
|
from ..utils import get_browser_protocol
|
||||||
from ..utils import token_authenticated
|
from ..utils import token_authenticated
|
||||||
from .base import APIHandler
|
from .base import APIHandler
|
||||||
from .base import BaseHandler
|
from .base import BaseHandler
|
||||||
@@ -115,7 +116,10 @@ class OAuthHandler:
|
|||||||
# make absolute local redirects full URLs
|
# make absolute local redirects full URLs
|
||||||
# to satisfy oauthlib's absolute URI requirement
|
# to satisfy oauthlib's absolute URI requirement
|
||||||
redirect_uri = (
|
redirect_uri = (
|
||||||
self.request.protocol + "://" + self.request.headers['Host'] + redirect_uri
|
get_browser_protocol(self.request)
|
||||||
|
+ "://"
|
||||||
|
+ self.request.host
|
||||||
|
+ redirect_uri
|
||||||
)
|
)
|
||||||
parsed_url = urlparse(uri)
|
parsed_url = urlparse(uri)
|
||||||
query_list = parse_qsl(parsed_url.query, keep_blank_values=True)
|
query_list = parse_qsl(parsed_url.query, keep_blank_values=True)
|
||||||
|
@@ -14,6 +14,7 @@ from tornado import web
|
|||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..handlers import BaseHandler
|
from ..handlers import BaseHandler
|
||||||
|
from ..utils import get_browser_protocol
|
||||||
from ..utils import isoformat
|
from ..utils import isoformat
|
||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ class APIHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
host_header = self.app.forwarded_host_header or "Host"
|
host_header = self.app.forwarded_host_header or "Host"
|
||||||
host = self.request.headers.get(host_header)
|
host = self.request.headers.get(host_header)
|
||||||
|
if host and "," in host:
|
||||||
|
host = host.split(",", 1)[0].strip()
|
||||||
referer = self.request.headers.get("Referer")
|
referer = self.request.headers.get("Referer")
|
||||||
|
|
||||||
# If no header is provided, assume it comes from a script/curl.
|
# If no header is provided, assume it comes from a script/curl.
|
||||||
@@ -71,7 +74,8 @@ class APIHandler(BaseHandler):
|
|||||||
self.log.warning("Blocking API request with no referer")
|
self.log.warning("Blocking API request with no referer")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
proto = self.request.protocol
|
proto = get_browser_protocol(self.request)
|
||||||
|
|
||||||
full_host = f"{proto}://{host}{self.hub.base_url}"
|
full_host = f"{proto}://{host}{self.hub.base_url}"
|
||||||
host_url = urlparse(full_host)
|
host_url = urlparse(full_host)
|
||||||
referer_url = urlparse(referer)
|
referer_url = urlparse(referer)
|
||||||
|
@@ -49,6 +49,7 @@ from ..spawner import LocalProcessSpawner
|
|||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import AnyTimeoutError
|
from ..utils import AnyTimeoutError
|
||||||
from ..utils import get_accepted_mimetype
|
from ..utils import get_accepted_mimetype
|
||||||
|
from ..utils import get_browser_protocol
|
||||||
from ..utils import maybe_future
|
from ..utils import maybe_future
|
||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
@@ -632,12 +633,10 @@ class BaseHandler(RequestHandler):
|
|||||||
next_url = self.get_argument('next', default='')
|
next_url = self.get_argument('next', default='')
|
||||||
# protect against some browsers' buggy handling of backslash as slash
|
# protect against some browsers' buggy handling of backslash as slash
|
||||||
next_url = next_url.replace('\\', '%5C')
|
next_url = next_url.replace('\\', '%5C')
|
||||||
if (next_url + '/').startswith(
|
proto = get_browser_protocol(self.request)
|
||||||
(
|
host = self.request.host
|
||||||
f'{self.request.protocol}://{self.request.host}/',
|
|
||||||
f'//{self.request.host}/',
|
if (next_url + '/').startswith((f'{proto}://{host}/', f'//{host}/',)) or (
|
||||||
)
|
|
||||||
) or (
|
|
||||||
self.subdomain_host
|
self.subdomain_host
|
||||||
and urlparse(next_url).netloc
|
and urlparse(next_url).netloc
|
||||||
and ("." + urlparse(next_url).netloc).endswith(
|
and ("." + urlparse(next_url).netloc).endswith(
|
||||||
|
@@ -53,6 +53,7 @@ from traitlets import validate
|
|||||||
from traitlets.config import SingletonConfigurable
|
from traitlets.config import SingletonConfigurable
|
||||||
|
|
||||||
from ..scopes import _intersect_expanded_scopes
|
from ..scopes import _intersect_expanded_scopes
|
||||||
|
from ..utils import get_browser_protocol
|
||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
|
|
||||||
@@ -772,7 +773,7 @@ class HubOAuth(HubAuth):
|
|||||||
# OAuth that doesn't complete shouldn't linger too long.
|
# OAuth that doesn't complete shouldn't linger too long.
|
||||||
'max_age': 600,
|
'max_age': 600,
|
||||||
}
|
}
|
||||||
if handler.request.protocol == 'https':
|
if get_browser_protocol(handler.request) == 'https':
|
||||||
kwargs['secure'] = True
|
kwargs['secure'] = True
|
||||||
# load user cookie overrides
|
# load user cookie overrides
|
||||||
kwargs.update(self.cookie_options)
|
kwargs.update(self.cookie_options)
|
||||||
@@ -812,7 +813,7 @@ class HubOAuth(HubAuth):
|
|||||||
def set_cookie(self, handler, access_token):
|
def set_cookie(self, handler, access_token):
|
||||||
"""Set a cookie recording OAuth result"""
|
"""Set a cookie recording OAuth result"""
|
||||||
kwargs = {'path': self.base_url, 'httponly': True}
|
kwargs = {'path': self.base_url, 'httponly': True}
|
||||||
if handler.request.protocol == 'https':
|
if get_browser_protocol(handler.request) == 'https':
|
||||||
kwargs['secure'] = True
|
kwargs['secure'] = True
|
||||||
# load user cookie overrides
|
# load user cookie overrides
|
||||||
kwargs.update(self.cookie_options)
|
kwargs.update(self.cookie_options)
|
||||||
|
@@ -98,27 +98,62 @@ async def test_post_content_type(app, content_type, status):
|
|||||||
|
|
||||||
|
|
||||||
@mark.parametrize(
|
@mark.parametrize(
|
||||||
"host, referer, status",
|
"host, referer, extraheaders, status",
|
||||||
[
|
[
|
||||||
('$host', '$url', 200),
|
('$host', '$url', {}, 200),
|
||||||
(None, None, 200),
|
(None, None, {}, 200),
|
||||||
(None, 'null', 403),
|
(None, 'null', {}, 403),
|
||||||
(None, 'http://attack.com/csrf/vulnerability', 403),
|
(None, 'http://attack.com/csrf/vulnerability', {}, 403),
|
||||||
('$host', {"path": "/user/someuser"}, 403),
|
('$host', {"path": "/user/someuser"}, {}, 403),
|
||||||
('$host', {"path": "{path}/foo/bar/subpath"}, 200),
|
('$host', {"path": "{path}/foo/bar/subpath"}, {}, 200),
|
||||||
# mismatch host
|
# mismatch host
|
||||||
("mismatch.com", "$url", 403),
|
("mismatch.com", "$url", {}, 403),
|
||||||
# explicit host, matches
|
# explicit host, matches
|
||||||
("fake.example", {"netloc": "fake.example"}, 200),
|
("fake.example", {"netloc": "fake.example"}, {}, 200),
|
||||||
# explicit port, matches implicit port
|
# explicit port, matches implicit port
|
||||||
("fake.example:80", {"netloc": "fake.example"}, 200),
|
("fake.example:80", {"netloc": "fake.example"}, {}, 200),
|
||||||
# explicit port, mismatch
|
# explicit port, mismatch
|
||||||
("fake.example:81", {"netloc": "fake.example"}, 403),
|
("fake.example:81", {"netloc": "fake.example"}, {}, 403),
|
||||||
# implicit ports, mismatch proto
|
# implicit ports, mismatch proto
|
||||||
("fake.example", {"netloc": "fake.example", "scheme": "https"}, 403),
|
("fake.example", {"netloc": "fake.example", "scheme": "https"}, {}, 403),
|
||||||
|
# explicit ports, match
|
||||||
|
("fake.example:81", {"netloc": "fake.example:81"}, {}, 200),
|
||||||
|
# Test proxy protocol defined headers taken into account by utils.get_browser_protocol
|
||||||
|
(
|
||||||
|
"fake.example",
|
||||||
|
{"netloc": "fake.example", "scheme": "https"},
|
||||||
|
{'X-Scheme': 'https'},
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fake.example",
|
||||||
|
{"netloc": "fake.example", "scheme": "https"},
|
||||||
|
{'X-Forwarded-Proto': 'https'},
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fake.example",
|
||||||
|
{"netloc": "fake.example", "scheme": "https"},
|
||||||
|
{
|
||||||
|
'Forwarded': 'host=fake.example;proto=https,for=1.2.34;proto=http',
|
||||||
|
'X-Scheme': 'http',
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fake.example",
|
||||||
|
{"netloc": "fake.example", "scheme": "https"},
|
||||||
|
{
|
||||||
|
'Forwarded': 'host=fake.example;proto=http,for=1.2.34;proto=http',
|
||||||
|
'X-Scheme': 'https',
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
("fake.example", {"netloc": "fake.example"}, {'X-Scheme': 'https'}, 403),
|
||||||
|
("fake.example", {"netloc": "fake.example"}, {'X-Scheme': 'https, http'}, 403),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_cors_check(request, app, host, referer, status):
|
async def test_cors_check(request, app, host, referer, extraheaders, status):
|
||||||
url = ujoin(public_host(app), app.hub.base_url)
|
url = ujoin(public_host(app), app.hub.base_url)
|
||||||
real_host = urlparse(url).netloc
|
real_host = urlparse(url).netloc
|
||||||
if host == "$host":
|
if host == "$host":
|
||||||
@@ -140,6 +175,7 @@ async def test_cors_check(request, app, host, referer, status):
|
|||||||
headers['X-Forwarded-Host'] = host
|
headers['X-Forwarded-Host'] = host
|
||||||
if referer is not None:
|
if referer is not None:
|
||||||
headers['Referer'] = referer
|
headers['Referer'] = referer
|
||||||
|
headers.update(extraheaders)
|
||||||
|
|
||||||
# add admin user
|
# add admin user
|
||||||
user = find_user(app.db, 'admin')
|
user = find_user(app.db, 'admin')
|
||||||
|
@@ -2,12 +2,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from async_generator import aclosing
|
from async_generator import aclosing
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.concurrent import run_on_executor
|
from tornado.concurrent import run_on_executor
|
||||||
|
from tornado.httpserver import HTTPRequest
|
||||||
|
from tornado.httputil import HTTPHeaders
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
from ..utils import iterate_until
|
from ..utils import iterate_until
|
||||||
|
|
||||||
|
|
||||||
@@ -88,3 +92,33 @@ async def test_tornado_coroutines():
|
|||||||
# verify that tornado gen and executor methods return awaitables
|
# verify that tornado gen and executor methods return awaitables
|
||||||
assert (await t.on_executor()) == "executor"
|
assert (await t.on_executor()) == "executor"
|
||||||
assert (await t.tornado_coroutine()) == "gen.coroutine"
|
assert (await t.tornado_coroutine()) == "gen.coroutine"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"forwarded, x_scheme, x_forwarded_proto, expected",
|
||||||
|
[
|
||||||
|
("", "", "", "_attr_"),
|
||||||
|
("for=1.2.3.4", "", "", "_attr_"),
|
||||||
|
("for=1.2.3.4,proto=https", "", "", "_attr_"),
|
||||||
|
("", "https", "http", "https"),
|
||||||
|
("", "https, http", "", "https"),
|
||||||
|
("", "https, http", "http", "https"),
|
||||||
|
("proto=http ; for=1.2.3.4, proto=https", "https, http", "", "http"),
|
||||||
|
("proto=invalid;for=1.2.3.4,proto=http", "https, http", "", "https"),
|
||||||
|
("for=1.2.3.4,proto=http", "https, http", "", "https"),
|
||||||
|
("", "invalid, http", "", "_attr_"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_browser_protocol(x_scheme, x_forwarded_proto, forwarded, expected):
|
||||||
|
request = Mock(spec=HTTPRequest)
|
||||||
|
request.protocol = "_attr_"
|
||||||
|
request.headers = HTTPHeaders()
|
||||||
|
if x_scheme:
|
||||||
|
request.headers["X-Scheme"] = x_scheme
|
||||||
|
if x_forwarded_proto:
|
||||||
|
request.headers["X-Forwarded-Proto"] = x_forwarded_proto
|
||||||
|
if forwarded:
|
||||||
|
request.headers["Forwarded"] = forwarded
|
||||||
|
|
||||||
|
proto = utils.get_browser_protocol(request)
|
||||||
|
assert proto == expected
|
||||||
|
@@ -355,7 +355,7 @@ def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
|
|||||||
h.update(btoken)
|
h.update(btoken)
|
||||||
digest = h.hexdigest()
|
digest = h.hexdigest()
|
||||||
|
|
||||||
return "{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
|
return f"{algorithm}:{rounds}:{salt}:{digest}"
|
||||||
|
|
||||||
|
|
||||||
def compare_token(compare, token):
|
def compare_token(compare, token):
|
||||||
@@ -683,3 +683,44 @@ def catch_db_error(f):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
return catching
|
return catching
|
||||||
|
|
||||||
|
|
||||||
|
def get_browser_protocol(request):
|
||||||
|
"""Get the _protocol_ seen by the browser
|
||||||
|
|
||||||
|
Like tornado's _apply_xheaders,
|
||||||
|
but in the case of multiple proxy hops,
|
||||||
|
use the outermost value (what the browser likely sees)
|
||||||
|
instead of the innermost value,
|
||||||
|
which is the most trustworthy.
|
||||||
|
|
||||||
|
We care about what the browser sees,
|
||||||
|
not where the request actually came from,
|
||||||
|
so trusting possible spoofs is the right thing to do.
|
||||||
|
"""
|
||||||
|
headers = request.headers
|
||||||
|
# first choice: Forwarded header
|
||||||
|
forwarded_header = headers.get("Forwarded")
|
||||||
|
if forwarded_header:
|
||||||
|
first_forwarded = forwarded_header.split(",", 1)[0].strip()
|
||||||
|
fields = {}
|
||||||
|
forwarded_dict = {}
|
||||||
|
for field in first_forwarded.split(";"):
|
||||||
|
key, _, value = field.partition("=")
|
||||||
|
fields[key.strip().lower()] = value.strip()
|
||||||
|
if "proto" in fields and fields["proto"].lower() in {"http", "https"}:
|
||||||
|
return fields["proto"].lower()
|
||||||
|
else:
|
||||||
|
app_log.warning(
|
||||||
|
f"Forwarded header present without protocol: {forwarded_header}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# second choice: X-Scheme or X-Forwarded-Proto
|
||||||
|
proto_header = headers.get("X-Scheme", headers.get("X-Forwarded-Proto", None))
|
||||||
|
if proto_header:
|
||||||
|
proto_header = proto_header.split(",")[0].strip().lower()
|
||||||
|
if proto_header in {"http", "https"}:
|
||||||
|
return proto_header
|
||||||
|
|
||||||
|
# no forwarded headers
|
||||||
|
return request.protocol
|
||||||
|
@@ -11,7 +11,7 @@ target_version = [
|
|||||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||||
|
|
||||||
[tool.tbump.version]
|
[tool.tbump.version]
|
||||||
current = "2.0.1"
|
current = "2.0.2"
|
||||||
|
|
||||||
# Example of a semver regexp.
|
# Example of a semver regexp.
|
||||||
# Make sure this matches current_version before
|
# Make sure this matches current_version before
|
||||||
|
@@ -1,20 +1,5 @@
|
|||||||
{% extends "page.html" %}
|
{% extends "page.html" %}
|
||||||
|
|
||||||
{% macro th(label, key='', colspan=1) %}
|
|
||||||
<th data-sort="{{key}}" colspan="{{colspan}}">{{label}}
|
|
||||||
{% if key %}
|
|
||||||
<a href="#"><i class="fa {% if sort.get(key) == 'asc' -%}
|
|
||||||
fa-sort-asc
|
|
||||||
{%- elif sort.get(key) == 'desc' -%}
|
|
||||||
fa-sort-desc
|
|
||||||
{%- else -%}
|
|
||||||
fa-sort
|
|
||||||
{%- endif %} sort-icon">
|
|
||||||
</i></a>
|
|
||||||
{% endif %}
|
|
||||||
</th>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div id="react-admin-hook">
|
<div id="react-admin-hook">
|
||||||
<script id="jupyterhub-admin-config">
|
<script id="jupyterhub-admin-config">
|
||||||
|
Reference in New Issue
Block a user