Compare commits

...

30 Commits
2.0.1 ... 2.0.2

Author SHA1 Message Date
Min RK
f5dc005a70 Bump to 2.0.2 2022-01-10 13:54:24 +01:00
Min RK
5fd8f0f596 Merge pull request #3759 from minrk/cl-202
changelog for 2.0.2
2022-01-10 13:53:34 +01:00
Min RK
26ceafa8a3 changelog for 2.0.2 2022-01-10 13:30:14 +01:00
Min RK
2e2ed8a4ff Merge pull request #3760 from minrk/admin-th-macro
remove unused macro with missing references
2022-01-10 13:28:10 +01:00
Min RK
6cc734f884 Merge pull request #3750 from consideRatio/pr/ci-refactor-docs-workflows
ci: refactor to avoid triggering all tests on changes to docs
2022-01-10 13:27:57 +01:00
Erik Sundell
4f7f07d3b7 Fix missing docs requirements 2022-01-10 11:18:22 +01:00
Min RK
d436c97e3d remove unused macro with missing references
The th macro is unused and doesn't work
because it references `sort` template variable,
which has been removed
2022-01-10 11:09:34 +01:00
Erik Sundell
807c5b8ff9 Make the generate-scope-table script autoformat its output 2022-01-10 10:48:01 +01:00
Erik Sundell
8da06d1259 Fix git CLI flag ordering 2022-01-10 10:33:23 +01:00
Erik Sundell
1c1be8a24b Generate yaml formatted to match prettier better 2022-01-10 10:31:30 +01:00
Min RK
897606b00c Merge pull request #3754 from jupyterhub/doc-theme-config
DOCS: Update theme configuration
2022-01-10 09:34:51 +01:00
Simon Li
615af5eb33 Merge pull request #3757 from minrk/get-browser-proto
use outermost proxied entry when looking up browser protocol
2022-01-09 22:44:07 +00:00
Erik Sundell
85f94c12fc Merge pull request #3748 from jupyterhub/DOC-allowed-users
DOC: Add note about allowed_users not being set
2022-01-08 18:59:24 +01:00
Min RK
ccfee4d235 use outermost proxied entry when checking for browser protocol
wee care about what the browser sees, so trust the outermost entry instead of the innermost

This is not secure _in general_, in that these values can be spoofed by malicious proxies,
but for CORS and cookie purposes, we only care about what the browser sees,
however many hops there may be.

A malicious proxy in the chain here isn't a concern because what matters is the immediate
hop from the _browser_, not the immediate hop from the _server_.
2022-01-07 14:03:11 +01:00
Min RK
a2ba55756d Merge pull request #3746 from manics/more-cors-tests
Extra test_cors_check tests
2022-01-07 12:37:37 +01:00
pre-commit-ci[bot]
1b3e94db6c [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-01-04 22:23:45 +00:00
Chris Holdgraf
614d9d89d0 DOCS: Update theme configuration 2022-01-04 14:22:45 -08:00
Chris Holdgraf
05a3f5aa9a Update docs/source/getting-started/authenticators-users-basics.md
Co-authored-by: Erik Sundell <erik.i.sundell@gmail.com>
2022-01-04 13:32:39 -08:00
Erik Sundell
4f47153123 ci: cleanup comments for readability 2022-01-04 00:53:33 +01:00
Erik Sundell
a14d9ecaa1 ci: refactor to avoid triggering all tests on changes to docs 2022-01-04 00:53:33 +01:00
Erik Sundell
6815f30d36 Merge pull request #3749 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-01-03 22:33:13 +01:00
pre-commit-ci[bot]
13172e6856 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-01-03 21:06:46 +00:00
pre-commit-ci[bot]
ebc9fd7758 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0)
2022-01-03 21:06:11 +00:00
Chris Holdgraf
0761a5db02 DOC: Add note about allowed_users not being set 2022-01-03 10:27:10 -08:00
Erik Sundell
46e7a231fe Merge pull request #3747 from minrk/https-typo
localhost URL is http, not https
2022-01-03 15:54:14 +01:00
Min RK
ffa5a20e2f localhost URL is not https 2022-01-03 15:41:54 +01:00
Simon Li
2088a57ffe Extra test_cors_check tests 2022-01-03 13:55:04 +00:00
Erik Sundell
345805781f Merge pull request #3740 from jupyterhub/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-12-27 22:53:25 +01:00
pre-commit-ci[bot]
9eb52ea788 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0)
2021-12-27 21:10:45 +00:00
Min RK
fb1405ecd8 Bump to 2.1.0.dev 2021-12-22 14:16:34 +01:00
22 changed files with 367 additions and 102 deletions

View File

@@ -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
View 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/

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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}

View File

@@ -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()

View File

@@ -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")),
], ],

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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">