mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
183 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e7689f277 | ||
![]() |
b677655572 | ||
![]() |
9adc871448 | ||
![]() |
29d6540333 | ||
![]() |
5a4949faa5 | ||
![]() |
f2ab23b376 | ||
![]() |
b61582420a | ||
![]() |
f11ae34b73 | ||
![]() |
e91ab50d1b | ||
![]() |
4cb3a45ce4 | ||
![]() |
4e8f9b4334 | ||
![]() |
6131f2dbaa | ||
![]() |
a9dc588454 | ||
![]() |
537b2eaff6 | ||
![]() |
7f8a981aed | ||
![]() |
bc86e4c8f5 | ||
![]() |
20f75c0018 | ||
![]() |
689dc5ba24 | ||
![]() |
d42a7261a4 | ||
![]() |
bcbf136de2 | ||
![]() |
55e9a0f5b5 | ||
![]() |
d64d916abc | ||
![]() |
da668b5e9a | ||
![]() |
d54442ecbf | ||
![]() |
c930d6bf6a | ||
![]() |
2ce263d45f | ||
![]() |
68f81fdc30 | ||
![]() |
e7ab18a720 | ||
![]() |
582467642c | ||
![]() |
d65e2daa15 | ||
![]() |
4eaa7c5eb3 | ||
![]() |
02de44e551 | ||
![]() |
4cdf0a65cd | ||
![]() |
b0367c21f3 | ||
![]() |
9d68107722 | ||
![]() |
ad61c23873 | ||
![]() |
c359221ef3 | ||
![]() |
cc94d290ab | ||
![]() |
da0a58cb9c | ||
![]() |
7ddd3b0589 | ||
![]() |
ff71d09fd1 | ||
![]() |
1eb0b1b073 | ||
![]() |
9ea9902c76 | ||
![]() |
6494017ce2 | ||
![]() |
b0cd9eebe9 | ||
![]() |
c3d4885521 | ||
![]() |
2919aaae79 | ||
![]() |
1986ba71c1 | ||
![]() |
a2c39a4dbc | ||
![]() |
1e847c8710 | ||
![]() |
83a8552a63 | ||
![]() |
f60c633320 | ||
![]() |
a5c7384228 | ||
![]() |
27de930978 | ||
![]() |
98e76d52bc | ||
![]() |
729aac9bd1 | ||
![]() |
bc85c445ab | ||
![]() |
9f708fa10c | ||
![]() |
d26c7cd6fc | ||
![]() |
0174083439 | ||
![]() |
e6fc2aee4a | ||
![]() |
47513cfbd0 | ||
![]() |
4e7147a495 | ||
![]() |
5cfc0db0d5 | ||
![]() |
eb862e2cbb | ||
![]() |
98799e4227 | ||
![]() |
ea6a0e53cc | ||
![]() |
f2b42a50c8 | ||
![]() |
43336f5b07 | ||
![]() |
bf2d948366 | ||
![]() |
271fd35bce | ||
![]() |
1d70986c25 | ||
![]() |
ec017d1f1d | ||
![]() |
a8c804de5b | ||
![]() |
3578001fab | ||
![]() |
b199110276 | ||
![]() |
b69bba5a7d | ||
![]() |
efdad701df | ||
![]() |
8a074b12b5 | ||
![]() |
b5e5fe630d | ||
![]() |
5d23bf6da3 | ||
![]() |
e5a8939481 | ||
![]() |
0eca901c65 | ||
![]() |
4a1964f881 | ||
![]() |
131094b5ff | ||
![]() |
4544a98fb9 | ||
![]() |
cbacdecb1e | ||
![]() |
64d8b2adc9 | ||
![]() |
9c83c15f67 | ||
![]() |
d2a545a01e | ||
![]() |
10e7ab96e5 | ||
![]() |
40f519544f | ||
![]() |
076c14dce6 | ||
![]() |
e223ce59e1 | ||
![]() |
ad833755e1 | ||
![]() |
142978b4d8 | ||
![]() |
e3cab48039 | ||
![]() |
203f4a5855 | ||
![]() |
cfc27db43d | ||
![]() |
e2a8557083 | ||
![]() |
d5478b1f21 | ||
![]() |
cf19af6f1c | ||
![]() |
1342f00d8e | ||
![]() |
1e49b4379b | ||
![]() |
a5d563217c | ||
![]() |
b1ac3b82dc | ||
![]() |
a376f33af1 | ||
![]() |
6f8a49569b | ||
![]() |
a4c553a5c5 | ||
![]() |
75ebe40f86 | ||
![]() |
69d711929a | ||
![]() |
4c12872dbf | ||
![]() |
21cee1be31 | ||
![]() |
00c782fd40 | ||
![]() |
b3f9635ecc | ||
![]() |
8c10fb285e | ||
![]() |
8a3f5d8f2e | ||
![]() |
7b496a5b4a | ||
![]() |
41445cffb4 | ||
![]() |
64e7705053 | ||
![]() |
dafd2d67f6 | ||
![]() |
823ab58f3a | ||
![]() |
ab7883e5c3 | ||
![]() |
8fd1fb3234 | ||
![]() |
6502b50576 | ||
![]() |
861347cce0 | ||
![]() |
43d4b65250 | ||
![]() |
e53ce19fcc | ||
![]() |
e603ff8274 | ||
![]() |
22b15f0ecf | ||
![]() |
c48c5bce99 | ||
![]() |
fa11d7e3c6 | ||
![]() |
7e3f29d033 | ||
![]() |
b7827687a8 | ||
![]() |
0beb4639a3 | ||
![]() |
b010c9501e | ||
![]() |
295e92270b | ||
![]() |
e42066f1c9 | ||
![]() |
1d29fcbfb2 | ||
![]() |
bdbfbb7e32 | ||
![]() |
42314ed75b | ||
![]() |
d8141692ab | ||
![]() |
025db2f9f3 | ||
![]() |
3985140377 | ||
![]() |
6886384ca3 | ||
![]() |
4a7fe8648a | ||
![]() |
7383c0cf60 | ||
![]() |
83186e02a2 | ||
![]() |
c6b4577c0a | ||
![]() |
73b1922c17 | ||
![]() |
1430e02fa8 | ||
![]() |
9ef09a288a | ||
![]() |
4a093be938 | ||
![]() |
64a253dbef | ||
![]() |
54877025ca | ||
![]() |
7793176b65 | ||
![]() |
bf32599d5d | ||
![]() |
01a31c894c | ||
![]() |
1e9cf23302 | ||
![]() |
555969141e | ||
![]() |
a938982bdc | ||
![]() |
17b54fee6a | ||
![]() |
60a153718d | ||
![]() |
9e1e382c37 | ||
![]() |
d72a96ec17 | ||
![]() |
5f845e78f1 | ||
![]() |
0d7e608a64 | ||
![]() |
15c5f152f8 | ||
![]() |
6d13893f16 | ||
![]() |
7e35de2577 | ||
![]() |
ec78503d1e | ||
![]() |
7d0bc1a112 | ||
![]() |
98e4531b44 | ||
![]() |
bb92058fbf | ||
![]() |
a5c59d6550 | ||
![]() |
f14be3df65 | ||
![]() |
3f7a32c990 | ||
![]() |
a8d8fc02e7 | ||
![]() |
0713fa209e | ||
![]() |
850f430ad6 | ||
![]() |
4026ed87e8 | ||
![]() |
f57d196e33 | ||
![]() |
ca9dc3a179 |
@@ -1,4 +1,4 @@
|
||||
# dependabot.yml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
# dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
#
|
||||
# Notes:
|
||||
# - Status and logs from dependabot are provided at
|
||||
@@ -8,8 +8,9 @@ version: 2
|
||||
updates:
|
||||
# Maintain dependencies in our GitHub Workflows
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
directory: /
|
||||
labels: [ci]
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: monthly
|
||||
time: "05:00"
|
||||
timezone: "Etc/UTC"
|
||||
timezone: Etc/UTC
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
|
||||
publish-docker:
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
|
||||
services:
|
||||
# So that we can test this in PRs/branches
|
||||
|
2
.github/workflows/support-bot.yml
vendored
2
.github/workflows/support-bot.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
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:
|
||||
Thank you for being an active member of our community! :heart:
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
issue-lock-reason: "off-topic"
|
||||
|
33
.github/workflows/test-docs.yml
vendored
33
.github/workflows/test-docs.yml
vendored
@@ -49,6 +49,11 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# make rediraffecheckdiff requires git history to compare current
|
||||
# commit with the main branch and previous releases.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
@@ -72,3 +77,31 @@ jobs:
|
||||
run: |
|
||||
cd docs
|
||||
make linkcheck
|
||||
|
||||
# make rediraffecheckdiff compares files for different changesets
|
||||
# these diff targets aren't always available
|
||||
# - compare with base ref (usually 'main', always on 'origin') for pull requests
|
||||
# - only compare with tags when running against jupyterhub/jupyterhub
|
||||
# to avoid errors on forks, which often lack tags
|
||||
- name: check redirects for this PR
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
cd docs
|
||||
export REDIRAFFE_BRANCH=origin/${{ github.base_ref }}
|
||||
make rediraffecheckdiff
|
||||
|
||||
# this should check currently published 'stable' links for redirects
|
||||
- name: check redirects since last release
|
||||
if: github.repository == 'jupyterhub/jupyterhub'
|
||||
run: |
|
||||
cd docs
|
||||
export REDIRAFFE_BRANCH=$(git describe --tags --abbrev=0)
|
||||
make rediraffecheckdiff
|
||||
|
||||
# longer-term redirect check (fixed version) for older links
|
||||
- name: check redirects since 3.0.0
|
||||
if: github.repository == 'jupyterhub/jupyterhub'
|
||||
run: |
|
||||
cd docs
|
||||
export REDIRAFFE_BRANCH=3.0.0
|
||||
make rediraffecheckdiff
|
||||
|
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
noextension: noextension
|
||||
subset: singleuser
|
||||
- python: "3.11"
|
||||
selenium: selenium
|
||||
browser: browser
|
||||
- python: "3.11"
|
||||
main_dependencies: main_dependencies
|
||||
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
fi
|
||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||
echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV
|
||||
echo "JUPYTERHUB_TEST_DB_URL=mysql+mysqlconnector://root@127.0.0.1:3306/jupyterhub" >> $GITHUB_ENV
|
||||
echo "JUPYTERHUB_TEST_DB_URL=mysql+mysqldb://root@127.0.0.1:3306/jupyterhub" >> $GITHUB_ENV
|
||||
fi
|
||||
if [ "${{ matrix.ssl }}" == "ssl" ]; then
|
||||
echo "SSL_ENABLED=1" >> $GITHUB_ENV
|
||||
@@ -164,7 +164,9 @@ jobs:
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.main_dependencies }}" != "" ]; then
|
||||
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
||||
# Tests are broken:
|
||||
# https://github.com/jupyterhub/jupyterhub/issues/4418
|
||||
# pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
|
||||
pip install --upgrade --pre sqlalchemy
|
||||
fi
|
||||
if [ "${{ matrix.legacy_notebook }}" != "" ]; then
|
||||
@@ -175,7 +177,7 @@ jobs:
|
||||
pip install "jupyter_server==${{ matrix.jupyter_server }}"
|
||||
fi
|
||||
if [ "${{ matrix.db }}" == "mysql" ]; then
|
||||
pip install mysql-connector-python
|
||||
pip install mysqlclient
|
||||
fi
|
||||
if [ "${{ matrix.db }}" == "postgres" ]; then
|
||||
pip install psycopg2-binary
|
||||
@@ -227,9 +229,13 @@ jobs:
|
||||
DB=postgres bash ci/init-db.sh
|
||||
fi
|
||||
|
||||
- name: Configure selenium tests
|
||||
if: matrix.selenium
|
||||
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m selenium" >> "${GITHUB_ENV}"
|
||||
- name: Configure browser tests
|
||||
if: matrix.browser
|
||||
run: echo "PYTEST_ADDOPTS=$PYTEST_ADDOPTS -m browser" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Ensure browsers are installed for playwright
|
||||
if: matrix.browser
|
||||
run: python -m playwright install --with-deps
|
||||
|
||||
- name: Run pytest
|
||||
run: |
|
||||
@@ -246,9 +252,8 @@ jobs:
|
||||
|
||||
- name: build images
|
||||
run: |
|
||||
docker build -t jupyterhub/jupyterhub .
|
||||
DOCKER_BUILDKIT=1 docker build -t jupyterhub/jupyterhub .
|
||||
docker build -t jupyterhub/jupyterhub-onbuild onbuild
|
||||
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
|
||||
docker build -t jupyterhub/singleuser singleuser
|
||||
|
||||
- name: smoke test jupyterhub
|
||||
|
@@ -16,7 +16,7 @@ ci:
|
||||
repos:
|
||||
# Autoformat: Python code, syntax patterns are modernized
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.4.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
@@ -24,7 +24,7 @@ repos:
|
||||
|
||||
# Autoformat: Python code
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.1
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
# args ref: https://github.com/PyCQA/autoflake#advanced-usage
|
||||
@@ -39,13 +39,13 @@ repos:
|
||||
|
||||
# Autoformat: Python code
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.4
|
||||
rev: v3.0.0-alpha.9-for-vscode
|
||||
hooks:
|
||||
- id: prettier
|
||||
|
||||
|
121
Dockerfile
121
Dockerfile
@@ -21,83 +21,116 @@
|
||||
# your jupyterhub_config.py will be added automatically
|
||||
# from your docker directory.
|
||||
|
||||
######################################################################
|
||||
# This Dockerfile uses multi-stage builds with optimisations to build
|
||||
# the JupyterHub wheel on the native architecture only
|
||||
# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
|
||||
|
||||
ARG BASE_IMAGE=ubuntu:22.04
|
||||
FROM $BASE_IMAGE AS builder
|
||||
|
||||
USER root
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -yq --no-install-recommends \
|
||||
######################################################################
|
||||
# The JupyterHub wheel is pure Python so can be built for any platform
|
||||
# on the native architecture (avoiding QEMU emulation)
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} $BASE_IMAGE AS jupyterhub-builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Don't clear apt cache, and don't combine RUN commands, so that cached layers can
|
||||
# be reused in other stages
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
locales \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-pycurl \
|
||||
python3-venv \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
|
||||
# Ubuntu 22.04 comes with Nodejs 12 which is too old for building JupyterHub JS
|
||||
# It's fine at runtime though (used only by configurable-http-proxy)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python3 -m pip install --upgrade setuptools pip build wheel
|
||||
RUN npm install --global yarn
|
||||
&& npm install --global yarn
|
||||
|
||||
WORKDIR /src/jupyterhub
|
||||
# copy everything except whats in .dockerignore, its a
|
||||
# compromise between needing to rebuild and maintaining
|
||||
# what needs to be part of the build
|
||||
COPY . /src/jupyterhub/
|
||||
WORKDIR /src/jupyterhub
|
||||
COPY . .
|
||||
|
||||
# Build client component packages (they will be copied into ./share and
|
||||
# packaged with the built wheel.)
|
||||
RUN python3 -m build --wheel
|
||||
RUN python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
|
||||
ARG PIP_CACHE_DIR=/tmp/pip-cache
|
||||
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
|
||||
python3 -m build --wheel
|
||||
|
||||
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
USER root
|
||||
######################################################################
|
||||
# All other wheels required by JupyterHub, some are platform specific
|
||||
FROM $BASE_IMAGE AS wheel-builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -yq --no-install-recommends \
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
locales \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-pycurl \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
python3-venv \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade setuptools pip build wheel
|
||||
|
||||
ENV SHELL=/bin/bash \
|
||||
WORKDIR /src/jupyterhub
|
||||
|
||||
COPY --from=jupyterhub-builder /src/jupyterhub/dist/*.whl /src/jupyterhub/dist/
|
||||
ARG PIP_CACHE_DIR=/tmp/pip-cache
|
||||
RUN --mount=type=cache,target=${PIP_CACHE_DIR} \
|
||||
python3 -m pip wheel --wheel-dir wheelhouse dist/*.whl
|
||||
|
||||
|
||||
######################################################################
|
||||
# The final JupyterHub image, platform specific
|
||||
FROM $BASE_IMAGE AS jupyterhub
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
SHELL=/bin/bash \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LANGUAGE=en_US.UTF-8
|
||||
|
||||
RUN locale-gen $LC_ALL
|
||||
|
||||
# always make sure pip is up to date!
|
||||
RUN python3 -m pip install --no-cache --upgrade setuptools pip
|
||||
|
||||
RUN npm install -g configurable-http-proxy@^4.2.0 \
|
||||
&& rm -rf ~/.npm
|
||||
|
||||
# install the wheels we built in the first stage
|
||||
COPY --from=builder /src/jupyterhub/wheelhouse /tmp/wheelhouse
|
||||
RUN python3 -m pip install --no-cache /tmp/wheelhouse/*
|
||||
|
||||
RUN mkdir -p /srv/jupyterhub/
|
||||
WORKDIR /srv/jupyterhub/
|
||||
LANGUAGE=en_US.UTF-8 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
LABEL maintainer="Jupyter Project <jupyter@googlegroups.com>"
|
||||
LABEL org.jupyter.service="jupyterhub"
|
||||
|
||||
WORKDIR /srv/jupyterhub
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
locales \
|
||||
python-is-python3 \
|
||||
python3-pip \
|
||||
python3-pycurl \
|
||||
nodejs \
|
||||
npm \
|
||||
&& locale-gen $LC_ALL \
|
||||
&& npm install -g configurable-http-proxy@^4.2.0 \
|
||||
# clean cache and logs
|
||||
&& rm -rf /var/lib/apt/lists/* /var/log/* /var/tmp/* ~/.npm
|
||||
# install the wheels we built in the previous stage
|
||||
RUN --mount=type=cache,from=wheel-builder,source=/src/jupyterhub/wheelhouse,target=/tmp/wheelhouse \
|
||||
# always make sure pip is up to date!
|
||||
python3 -m pip install --no-compile --no-cache-dir --upgrade setuptools pip \
|
||||
&& python3 -m pip install --no-compile --no-cache-dir /tmp/wheelhouse/*
|
||||
|
||||
CMD ["jupyterhub"]
|
||||
|
@@ -12,7 +12,7 @@ print(f"DATA_FILES_PATH={DATA_FILES_PATH}", end=" ")
|
||||
DATA_FILES_PATH = Path(DATA_FILES_PATH)
|
||||
assert DATA_FILES_PATH.is_dir(), DATA_FILES_PATH
|
||||
for subpath in (
|
||||
"templates/page.html",
|
||||
"templates/spawn.html",
|
||||
"static/css/style.min.css",
|
||||
"static/components/jquery/dist/jquery.js",
|
||||
"static/js/admin-react.js",
|
||||
@@ -28,6 +28,7 @@ for subpath in (
|
||||
"alembic.ini",
|
||||
"alembic/versions/833da8570507_rbac.py",
|
||||
"event-schemas/server-actions/v1.yaml",
|
||||
"singleuser/templates/page.html",
|
||||
):
|
||||
path = jupyterhub_path / subpath
|
||||
assert path.is_file(), path
|
||||
|
@@ -1,14 +0,0 @@
|
||||
FROM alpine:3.13
|
||||
ENV LANG=en_US.UTF-8
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-ruamel.yaml \
|
||||
py3-cryptography \
|
||||
py3-sqlalchemy
|
||||
|
||||
ARG JUPYTERHUB_VERSION=1.3.0
|
||||
RUN pip3 install --no-cache jupyterhub==${JUPYTERHUB_VERSION}
|
||||
|
||||
USER nobody
|
||||
CMD ["jupyterhub"]
|
@@ -1,22 +0,0 @@
|
||||
## What is Dockerfile.alpine
|
||||
|
||||
Dockerfile.alpine contains the base image for jupyterhub. It does not work independently, but only as part of a full jupyterhub cluster
|
||||
|
||||
## How to use it?
|
||||
|
||||
You will need:
|
||||
|
||||
1. A running configurable-http-proxy, whose API is accessible.
|
||||
2. A jupyterhub_config file.
|
||||
3. Authentication and other libraries required by the specific jupyterhub_config file.
|
||||
|
||||
## Steps to test it outside a cluster
|
||||
|
||||
- start configurable-http-proxy in another container
|
||||
- specify CONFIGPROXY_AUTH_TOKEN env in both containers
|
||||
- put both containers on the same network (e.g. docker network create jupyterhub; docker run ... --net jupyterhub)
|
||||
- tell jupyterhub where CHP is (e.g. c.ConfigurableHTTPProxy.api_url = 'http://chp:8001')
|
||||
- tell jupyterhub not to start the proxy itself (c.ConfigurableHTTPProxy.should_start = False)
|
||||
- Use a dummy authenticator for ease of testing. Update following in jupyterhub_config file
|
||||
- c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
|
||||
- c.DummyAuthenticator.password = "your strong password"
|
@@ -6,7 +6,7 @@ info:
|
||||
description: The REST API for JupyterHub
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
version: 4.0.0b1
|
||||
version: 4.0.2
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
@@ -1202,13 +1202,13 @@ components:
|
||||
description: Timestamp of last-seen activity from the user
|
||||
format: date-time
|
||||
servers:
|
||||
type: array
|
||||
type: object
|
||||
description: |
|
||||
The servers for this user.
|
||||
By default: only includes _active_ servers.
|
||||
Changed in 3.0: if `?include_stopped_servers` parameter is specified,
|
||||
stopped servers will be included as well.
|
||||
items:
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/Server"
|
||||
auth_state:
|
||||
type: object
|
||||
|
@@ -50,6 +50,7 @@ myst_heading_anchors = 2
|
||||
|
||||
myst_enable_extensions = [
|
||||
# available extensions: https://myst-parser.readthedocs.io/en/latest/syntax/optional.html
|
||||
"attrs_inline",
|
||||
"colon_fence",
|
||||
"deflist",
|
||||
"fieldlist",
|
||||
@@ -185,6 +186,7 @@ linkcheck_ignore = [
|
||||
"https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog
|
||||
"https://github.com/jupyterhub/jupyterhub/compare/", # too many comparisons in changelog
|
||||
r"https?://(localhost|127.0.0.1).*", # ignore localhost references in auto-links
|
||||
r".*/rest-api.html#.*", # ignore javascript-resolved internal rest-api links
|
||||
r"https://jupyter.chameleoncloud.org", # FIXME: ignore (presumably) short-term SSL issue
|
||||
]
|
||||
linkcheck_anchors_ignore = [
|
||||
@@ -199,6 +201,7 @@ intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
"tornado": ("https://www.tornadoweb.org/en/stable/", None),
|
||||
"jupyter-server": ("https://jupyter-server.readthedocs.io/en/stable/", None),
|
||||
"nbgitpuller": ("https://nbgitpuller.readthedocs.io/en/latest", None),
|
||||
}
|
||||
|
||||
# -- Options for the opengraph extension -------------------------------------
|
||||
@@ -233,8 +236,12 @@ ogp_use_first_image = True
|
||||
# If you are basing changes off another branch/ commit, always change back
|
||||
# rediraffe_branch to main before pushing your changes upstream.
|
||||
#
|
||||
rediraffe_branch = "main"
|
||||
rediraffe_branch = os.environ.get("REDIRAFFE_BRANCH", "main")
|
||||
rediraffe_redirects = "redirects.txt"
|
||||
|
||||
# allow 80% match for autogenerated redirects
|
||||
rediraffe_auto_redirect_perc = 80
|
||||
|
||||
# rediraffe_redirects = {
|
||||
# "old-file": "new-folder/new-file-name",
|
||||
# }
|
||||
|
@@ -130,8 +130,8 @@ configuration:
|
||||
jupyterhub -f testing/jupyterhub_config.py
|
||||
```
|
||||
|
||||
The default JupyterHub [authenticator](https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#the-default-pam-authenticator)
|
||||
& [spawner](https://jupyterhub.readthedocs.io/en/stable/api/spawner.html#localprocessspawner)
|
||||
The default JupyterHub [authenticator](PAMAuthenticator)
|
||||
& [spawner](LocalProcessSpawner)
|
||||
require your system to have user accounts for each user you want to log in to
|
||||
JupyterHub as.
|
||||
|
||||
|
@@ -82,7 +82,7 @@ Additionally, there is usually _very_ little load on the database itself.
|
||||
By far the most taxing activity on the database is the 'list all users' endpoint, primarily used by the [idle-culling service](https://github.com/jupyterhub/jupyterhub-idle-culler).
|
||||
Database-based optimizations have been added to make even these operations feasible for large numbers of users:
|
||||
|
||||
1. State filtering on [GET /users](jupyterhub-rest-API) with `?state=active`,
|
||||
1. State filtering on [GET /hub/api/users?state=active](../reference/rest-api.html#/default/get_users){.external},
|
||||
which limits the number of results in the query to only the relevant subset (added in JupyterHub 1.3), rather than all users.
|
||||
2. [Pagination](api-pagination) of all list endpoints, allowing the request of a large number of resources to be more fairly balanced with other Hub activities across multiple requests (added in 2.0).
|
||||
|
||||
@@ -95,8 +95,14 @@ The Hub and its database are not involved in most requests to single-user server
|
||||
|
||||
JupyterHub supports a variety of database backends via [SQLAlchemy][].
|
||||
The default is sqlite, which works great for many cases, but you should be able to use many backends supported by SQLAlchemy.
|
||||
Usually, this will mean PostgreSQL or MySQL, both of which are well tested with JupyterHub.
|
||||
Usually, this will mean PostgreSQL or MySQL, both of which are officially supported and well tested with JupyterHub, but others may work as well.
|
||||
See [SQLAlchemy's docs][sqlalchemy-dialect] for how to connect to different database backends.
|
||||
Doing so generally involves:
|
||||
|
||||
1. installing a Python package that provides a client implementation, and
|
||||
2. setting [](JupyterHub.db_url) to connect to your database with the specified implementation
|
||||
|
||||
[sqlalchemy-dialect]: https://docs.sqlalchemy.org/en/20/dialects/
|
||||
[sqlalchemy]: https://www.sqlalchemy.org
|
||||
|
||||
### Default backend: SQLite
|
||||
@@ -109,14 +115,16 @@ For production systems, SQLite has some disadvantages when used with JupyterHub:
|
||||
|
||||
- `upgrade-db` may not always work, and you may need to start with a fresh database
|
||||
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
||||
version, so backup the `jupyterhub.sqlite` file before upgrading
|
||||
version, so backup the `jupyterhub.sqlite` file before upgrading (JupyterHub automatically creates a date-stamped backup file when upgrading sqlite)
|
||||
|
||||
The sqlite documentation provides a helpful page about [when to use SQLite and
|
||||
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
||||
|
||||
### Picking your database backend (PostgreSQL, MySQL)
|
||||
|
||||
When running a long term deployment or a production system, we recommend using a full-fledged relational database, such as [PostgreSQL](https://www.postgresql.org) or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE` statement.
|
||||
When running a long term deployment or a production system, we recommend using a full-fledged relational database, such as [PostgreSQL](https://www.postgresql.org) or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE` statement, which is used in some database upgrade steps.
|
||||
|
||||
In general, you select your database backend with [](JupyterHub.db_url), and can further configure it (usually not necessary) with [](JupyterHub.db_kwargs).
|
||||
|
||||
## Notes and Tips
|
||||
|
||||
@@ -132,14 +140,25 @@ multiple processes which might try to access the file at the same time.
|
||||
### PostgreSQL
|
||||
|
||||
We recommend using PostgreSQL for production if you are unsure whether to use
|
||||
MySQL or PostgreSQL or if you do not have a strong preference. There is
|
||||
additional configuration required for MySQL that is not needed for PostgreSQL.
|
||||
MySQL or PostgreSQL or if you do not have a strong preference.
|
||||
There is additional configuration required for MySQL that is not needed for PostgreSQL.
|
||||
|
||||
For example, to connect to a postgres database with psycopg2:
|
||||
|
||||
1. install psycopg2: `pip instal psycopg2` (or `psycopg2-binary` to avoid compilation, which is [not recommended for production][psycopg2-binary])
|
||||
2. set authentication via environment variables `PGUSER` and `PGPASSWORD`
|
||||
3. configure [](JupyterHub.db_url):
|
||||
|
||||
```python
|
||||
c.JupyterHub.db_url = "postgres+psycopg2://my-postgres-server:5432/my-db-name"
|
||||
```
|
||||
|
||||
[psycopg2-binary]: https://www.psycopg.org/docs/install.html#psycopg-vs-psycopg-binary
|
||||
|
||||
### MySQL / MariaDB
|
||||
|
||||
- You should use the `pymysql` sqlalchemy provider (the other one, MySQLdb,
|
||||
isn't available for py3).
|
||||
- You also need to set `pool_recycle` to some value (typically 60 - 300)
|
||||
- You should probably use the `pymysql` or `mysqlclient` sqlalchemy provider, or another backend [recommended by sqlalchemy](https://docs.sqlalchemy.org/en/20/dialects/mysql.html#dialect-mysql)
|
||||
- You also need to set `pool_recycle` to some value (typically 60 - 300, JupyterHub will default to 60)
|
||||
which depends on your MySQL setup. This is necessary since MySQL kills
|
||||
connections serverside if they've been idle for a while, and the connection
|
||||
from the hub will be idle for longer than most connections. This behavior
|
||||
@@ -153,3 +172,12 @@ additional configuration required for MySQL that is not needed for PostgreSQL.
|
||||
correctly. Later versions of MariaDB and MySQL should set these values by
|
||||
default, as well as have a default `DYNAMIC` `row_format` and pose no trouble
|
||||
to users.
|
||||
|
||||
For example, to connect to a mysql database with mysqlclient:
|
||||
|
||||
1. install mysqlclient: `pip install mysqlclient`
|
||||
2. configure [](JupyterHub.db_url):
|
||||
|
||||
```python
|
||||
c.JupyterHub.db_url = "mysql+mysqldb://myuser:mypassword@my-sql-server:3306/my-db-name"
|
||||
```
|
||||
|
@@ -2,35 +2,75 @@
|
||||
|
||||
## How do I share links to notebooks?
|
||||
|
||||
In short, where you see `/user/name/notebooks/foo.ipynb` use `/hub/user-redirect/notebooks/foo.ipynb` (replace `/user/name` with `/hub/user-redirect`).
|
||||
|
||||
Sharing links to notebooks is a common activity,
|
||||
and can look different based on what you mean.
|
||||
and can look different depending on what you mean by 'share.'
|
||||
Your first instinct might be to copy the URL you see in the browser,
|
||||
e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`.
|
||||
However, let's break down what this URL means:
|
||||
e.g. `jupyterhub.example/user/yourname/notebooks/coolthing.ipynb`,
|
||||
but this usually won't work, depending on the permissions of the person you share the link with.
|
||||
|
||||
`hub.jupyter.org/user/yourname/` is the URL prefix handled by _your server_,
|
||||
which means that sharing this URL is asking the person you share the link with
|
||||
to come to _your server_ and look at the exact same file.
|
||||
In most circumstances, this is forbidden by permissions because the person you share with does not have access to your server.
|
||||
What actually happens when someone visits this URL will depend on whether your server is running and other factors.
|
||||
Unfortunately, 'share' means at least a few things to people in a JupyterHub context.
|
||||
We'll cover 3 common cases here, when they are applicable, and what assumptions they make:
|
||||
|
||||
**But what is our actual goal?**
|
||||
1. sharing links that will open the same file on the visitor's own server
|
||||
2. sharing links that will bring the visitor to _your_ server (e.g. for real-time collaboration, or RTC)
|
||||
3. publishing notebooks and sharing links that will download the notebook into the user's server
|
||||
|
||||
A typical situation is that you have some shared or common filesystem,
|
||||
such that the same path corresponds to the same document
|
||||
(either the exact same document or another copy of it).
|
||||
Typically, what folks want when they do sharing like this
|
||||
is for each visitor to open the same file _on their own server_,
|
||||
so Breq would open `/user/breq/notebooks/foo.ipynb` and
|
||||
Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc.
|
||||
### link to the same file on the visitor's server
|
||||
|
||||
JupyterHub has a special URL that does exactly this!
|
||||
It's called `/hub/user-redirect/...`.
|
||||
So if you replace `/user/yourname` in your URL bar
|
||||
with `/hub/user-redirect` any visitor should get the same
|
||||
URL on their own server, rather than visiting yours.
|
||||
This is for the case where you have JupyterHub on a shared (or sufficiently similar) filesystem, where you want to share a link that will cause users to login and start their _own_ server, to view or edit the file.
|
||||
|
||||
In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link"
|
||||
action in the file browser.
|
||||
**Assumption:** the same path on someone else's server is valid and points to the same file
|
||||
|
||||
This is useful in e.g. classes where you know students have certain files in certain locations, or collaborations where you know you have a shared filesystem where everyone has access to the same files.
|
||||
|
||||
A link should look like `https://jupyterhub.example/hub/user-redirect/lab/tree/foo.ipynb`.
|
||||
You can hand-craft these URLs from the URL you are looking at, where you see `/user/name/lab/tree/foo.ipynb` use `/hub/user-redirect/lab/tree/foo.ipynb` (replace `/user/name/` with `/hub/user-redirect/`).
|
||||
Or you can use JupyterLab's "copy shareable link" in the context menu in the file browser:
|
||||
|
||||

|
||||
|
||||
which will produce a correct URL with `/hub/user-redirect/` in it.
|
||||
|
||||
### link to the file on your server
|
||||
|
||||
This is for the case where you want to both be using _your_ server, e.g. for real-time collaboration (RTC).
|
||||
|
||||
**Assumption:** the user has (or should have) access to your server.
|
||||
|
||||
**Assumption:** your server is running _or_ the user has permission to start it.
|
||||
|
||||
By default, JupyterHub users don't have access to each other's servers, but JupyterHub 2.0 administrators can grant users limited access permissions to each other's servers.
|
||||
If the visitor doesn't have access to the server, these links will result in a 403 Permission Denied error.
|
||||
|
||||
In many cases, for this situation you can copy the link in your URL bar (`/user/yourname/lab`), or you can add `/tree/path/to/specific/notebook.ipynb` to open a specific file.
|
||||
|
||||
The [jupyterlab-link-share] JupyterLab extension generates these links, and even can _grant_ other users access to your server.
|
||||
|
||||
[jupyterlab-link-share]: https://github.com/jupyterlab-contrib/jupyterlab-link-share
|
||||
|
||||
:::{warning}
|
||||
Note that the way the extension _grants_ access is handing over credentials to allow the other user to **_BECOME YOU_**.
|
||||
This is usually not appropriate in JupyterHub.
|
||||
:::
|
||||
|
||||
### link to a published copy
|
||||
|
||||
Another way to 'share' notebooks is to publish copies, e.g. pushing the notebook to a git repository and sharing a download link.
|
||||
This way is especially useful for course materials,
|
||||
where no assumptions are necessary about the user's environment,
|
||||
except for having one package installed.
|
||||
|
||||
**Assumption:** The [nbgitpuller](inv:nbgitpuller#index) server extension is installed
|
||||
|
||||
Unlike the other two methods, nbgitpuller doesn't provide an extension to copy a shareable link for the document you're currently looking at,
|
||||
but it does provide a [link generator](inv:nbgitpuller#link),
|
||||
which uses the `user-redirect` approach above.
|
||||
|
||||
When visiting an nbgitpuller link:
|
||||
|
||||
- The visitor will be directed to their own server
|
||||
- Your repo will be cloned (or updated if it's already been cloned)
|
||||
- and then the file opened when it's ready
|
||||
|
||||
[nbgitpuller]: https://nbgitpuller.readthedocs.io
|
||||
[nbgitpuller-link]: https://nbgitpuller.readthedocs.io/en/latest/link.html
|
||||
|
@@ -66,7 +66,7 @@ Here is a sample of organizations that use JupyterHub:
|
||||
- **Universities and colleges**: UC Berkeley, UC San Diego, Cal Poly SLO, Harvard University, University of Chicago,
|
||||
University of Oslo, University of Sheffield, Université Paris Sud, University of Versailles
|
||||
- **Research laboratories**: NASA, NCAR, NOAA, the Large Synoptic Survey Telescope, Brookhaven National Lab,
|
||||
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory
|
||||
Minnesota Supercomputing Institute, ALCF, CERN, Lawrence Livermore National Laboratory, HUNT
|
||||
- **Online communities**: Pangeo, Quantopian, mybinder.org, MathHub, Open Humans
|
||||
- **Computing infrastructure providers**: NERSC, San Diego Supercomputing Center, Compute Canada
|
||||
- **Companies**: Capital One, SANDVIK code, Globus
|
||||
@@ -124,13 +124,13 @@ as more resources are needed - allowing you to utilize the benefits of a flexibl
|
||||
|
||||
### Is JupyterHub secure?
|
||||
|
||||
The short answer: yes.
|
||||
The short answer: yes.
|
||||
JupyterHub as a standalone application has been battle-tested at an institutional
|
||||
level for several years, and makes a number of "default" security decisions that are reasonable for most
|
||||
users.
|
||||
|
||||
- For security considerations in the base JupyterHub application,
|
||||
[see the JupyterHub security page](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html).
|
||||
[see the JupyterHub security page](web-security).
|
||||
- For security considerations when deploying JupyterHub on Kubernetes, see the
|
||||
[JupyterHub on Kubernetes security page](https://z2jh.jupyter.org/en/latest/security.html).
|
||||
|
||||
|
@@ -167,7 +167,7 @@ When your whole JupyterHub sits behind an organization proxy (_not_ a reverse pr
|
||||
|
||||
### Launching Jupyter Notebooks to run as an externally managed JupyterHub service with the `jupyterhub-singleuser` command returns a `JUPYTERHUB_API_TOKEN` error
|
||||
|
||||
[JupyterHub services](https://jupyterhub.readthedocs.io/en/stable/reference/services.html) allow processes to interact with JupyterHub's REST API. Example use-cases include:
|
||||
{ref}`services` allow processes to interact with JupyterHub's REST API. Example use-cases include:
|
||||
|
||||
- **Secure Testing**: provide a canonical Jupyter Notebook for testing production data to reduce the number of entry points into production systems.
|
||||
- **Grading Assignments**: provide access to shared Jupyter Notebooks that may be used for management tasks such as grading assignments.
|
||||
|
@@ -45,7 +45,7 @@ additional packages.
|
||||
|
||||
## Configuring Jupyter and IPython
|
||||
|
||||
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)
|
||||
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/configuring/config_overview.html)
|
||||
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
||||
have their own configuration systems.
|
||||
|
||||
@@ -212,13 +212,31 @@ By default, the single-user server launches JupyterLab,
|
||||
which is based on [Jupyter Server][].
|
||||
|
||||
This is the default server when running JupyterHub ≥ 2.0.
|
||||
To switch to using the legacy Jupyter Notebook server, you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
||||
To switch to using the legacy Jupyter Notebook server (notebook < 7.0), you can set the `JUPYTERHUB_SINGLEUSER_APP` environment variable
|
||||
(in the single-user environment) to:
|
||||
|
||||
```bash
|
||||
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||
```
|
||||
|
||||
:::{note}
|
||||
|
||||
```
|
||||
JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
|
||||
```
|
||||
|
||||
is only valid for notebook < 7. notebook v7 is based on jupyter-server,
|
||||
and the default jupyter-server application must be used.
|
||||
Selecting the new notebook UI is no longer a matter of selecting the server app to launch,
|
||||
but only the default URL for users to visit.
|
||||
To use notebook v7 with JupyterHub, leave the default singleuser app config alone (or specify `JUPYTERHUB_SINGLEUSER_APP=jupyter-server`) and set the default _URL_ for user servers:
|
||||
|
||||
```python
|
||||
c.Spawner.default_url = '/tree/'
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
[jupyter server]: https://jupyter-server.readthedocs.io
|
||||
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
|
||||
|
||||
|
@@ -33,36 +33,13 @@ such as:
|
||||
To send requests using the JupyterHub API, you must pass an API token with
|
||||
the request.
|
||||
|
||||
The preferred way of generating an API token is by running:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This `openssl` command generates a potential token that can then be
|
||||
added to JupyterHub using `.api_tokens` configuration setting in
|
||||
`jupyterhub_config.py`.
|
||||
|
||||
```{note}
|
||||
The api_tokens configuration has been softly deprecated since the introduction of services.
|
||||
```
|
||||
|
||||
Alternatively, you can use the `jupyterhub token` command to generate a token
|
||||
for a specific hub user by passing the **username**:
|
||||
|
||||
```bash
|
||||
jupyterhub token <username>
|
||||
```
|
||||
|
||||
This command generates a random string to use as a token and registers
|
||||
it for the given user with the Hub's database.
|
||||
|
||||
In [version 0.8.0](changelog), a token request page for
|
||||
generating an API token is available from the JupyterHub user interface:
|
||||
While JupyterHub is running, any JupyterHub user can request a token via the `token` page.
|
||||
This is accessible via a `token` link in the top nav bar from the JupyterHub home page,
|
||||
or at the URL `/hub/token`.
|
||||
|
||||
:::{figure-md}
|
||||
|
||||

|
||||

|
||||
|
||||
JupyterHub's API token page
|
||||
:::
|
||||
@@ -74,6 +51,40 @@ JupyterHub's token page after successfully requesting a token.
|
||||
|
||||
:::
|
||||
|
||||
### Register API tokens via configuration
|
||||
|
||||
Sometimes, you'll want to pre-generate a token for access to JupyterHub,
|
||||
typically for use by external services,
|
||||
so that both JupyterHub and the service have access to the same value.
|
||||
|
||||
First, you need to generate a good random secret.
|
||||
A good way of generating an API token is by running:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This `openssl` command generates a random token that can be added to the JupyterHub configuration in `jupyterhub_config.py`.
|
||||
|
||||
For external services, this would be registered with JupyterHub via configuration:
|
||||
|
||||
```python
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
"name": "my-service",
|
||||
"api_token": the_secret_value,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
At this point, requests authenticated with the token will be associated with The service `my-service`.
|
||||
|
||||
```{note}
|
||||
You can also load additional tokens for users via the `JupyterHub.api_tokens` configuration.
|
||||
|
||||
However, this option has been deprecated since the introduction of services.
|
||||
```
|
||||
|
||||
## Assigning permissions to a token
|
||||
|
||||
Prior to JupyterHub 2.0, there were two levels of permissions:
|
||||
|
BIN
docs/source/images/shareable_link.webp
Normal file
BIN
docs/source/images/shareable_link.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 137 KiB |
Binary file not shown.
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
Before Width: | Height: | Size: 52 KiB |
@@ -1,23 +1,54 @@
|
||||
# This file contains rediraffe redirects as generated from the docs/source/conf.py file
|
||||
# For more information, see rediraffe configuration in the conf.py file.
|
||||
|
||||
|
||||
"changelog.md" "reference/changelog.md"
|
||||
"contributor-list.md" "contributing/contributor-list.md"
|
||||
"gallery-jhub-deployments.md" "reference/gallery-jhub-deployments.md"
|
||||
"installation-basics.md" "tutorial/installation-basics.md"
|
||||
"quickstart.md" "tutorial/quickstart.md"
|
||||
"quickstart-docker.md" "tutorial/quickstart-docker.md"
|
||||
"troubleshooting.md" "faq/troubleshooting.md"
|
||||
|
||||
"admin/capacity-planning.md" "explanation/capacity-planning.md"
|
||||
"admin/log-messages.md" "howto/log-messages.md"
|
||||
"admin/upgrading.md" "howto/upgrading.md"
|
||||
|
||||
"events/index.md" "reference/event-logging.md"
|
||||
|
||||
"getting-started/authenticators-users-basics.md" "tutorial/getting-started/authenticators-users-basics.md"
|
||||
"getting-started/config-basics.md" "tutorial/getting-started/config-basics.md"
|
||||
"getting-started/faq.md" "faq/faq.md"
|
||||
"getting-started/institutional-faq.md" "faq/institutional-faq.md"
|
||||
"getting-started/networking-basics.md" "tutorial/getting-started/networking-basics.md"
|
||||
"getting-started/services-basics.md" "tutorial/getting-started/services-basics.md"
|
||||
"getting-started/spawners-basics.md" "tutorial/getting-started/spawners-basics.md"
|
||||
|
||||
"reference/api-only.md" "howto/api-only.md"
|
||||
"reference/config-ghoauth.md" "howto/configuration/config-ghoauth.md"
|
||||
"reference/config-proxy.md" "howto/configuration/config-proxy.md"
|
||||
"admin/log-messages.md" "howto/log-messages.md"
|
||||
"reference/database.md" "explanation/database.md"
|
||||
"reference/oauth.md" "explanation/oauth.md"
|
||||
"reference/proxy.md" "howto/proxy.md"
|
||||
"reference/templates.md" "howto/templates.md"
|
||||
"quickstart-docker.md" "tutorial/quickstart-docker.md"
|
||||
"reference/config-examples.md" "howto/index.md"
|
||||
"getting-started/institutional-faq.md" "faq/institutional-faq.md"
|
||||
"troubleshooting.md" "faq/troubleshooting.md"
|
||||
"reference/config-sudo.md" "howto/configuration/config-sudo.md"
|
||||
"reference/config-user-env.md" "howto/configuration/config-user-env.md"
|
||||
"reference/rest.md" "howto/rest.md"
|
||||
"reference/separate-proxy.md" "howto/separate-proxy.md"
|
||||
"admin/upgrading.md" "howto/upgrading.md"
|
||||
"installation-basics.md" "tutorial/installation-basics.md"
|
||||
"quickstart.md" "tutorial/quickstart.md"
|
||||
"events/index.md" "reference/event-logging.md"
|
||||
"reference/server-api.md" "tutorial/server-api.md"
|
||||
"reference/websecurity.md" "explanation/websecurity.md"
|
||||
|
||||
"api/app.md" "reference/api/app.md"
|
||||
"api/auth.md" "reference/api/auth.md"
|
||||
"api/index.md" "reference/api/index.md"
|
||||
"api/proxy.md" "reference/api/proxy.md"
|
||||
"api/service.md" "reference/api/service.md"
|
||||
"api/services.auth.md" "reference/api/services.auth.md"
|
||||
"api/spawner.md" "reference/api/spawner.md"
|
||||
"api/user.md" "reference/api/user.md"
|
||||
|
||||
# -- JupyterHub 4.0 --
|
||||
# redirects above are up-to-date as of JupyterHub 4.0
|
||||
# add future redirects below
|
||||
# (e.g. with `make rediraffewritediff`)
|
||||
|
@@ -273,7 +273,7 @@ c.Spawner.auth_state_hook = auth_state_hook
|
||||
:::
|
||||
|
||||
Some identity providers may have their own concept of group membership that you would like to preserve in JupyterHub.
|
||||
This is now possible with `Authenticator.managed_groups`.
|
||||
This is now possible with `Authenticator.manage_groups`.
|
||||
|
||||
You can set the config:
|
||||
|
||||
|
@@ -8,9 +8,92 @@ command line for details.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 4.0 (beta) - 2023-XX-YY
|
||||
## 4.0
|
||||
|
||||
### 4.0.2 - 2023-08-10
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.1...4.0.2))
|
||||
|
||||
#### Enhancements made
|
||||
|
||||
- avoid counting failed requests to not-running servers as 'activity' [#4491](https://github.com/jupyterhub/jupyterhub/pull/4491) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- improve permission-denied errors for various cases [#4489](https://github.com/jupyterhub/jupyterhub/pull/4489) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- set root_dir when using singleuser extension [#4503](https://github.com/jupyterhub/jupyterhub/pull/4503) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
||||
- Allow setting custom log_function in tornado_settings in SingleUserServer [#4475](https://github.com/jupyterhub/jupyterhub/pull/4475) ([@grios-stratio](https://github.com/grios-stratio), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- doc: update notebook config URL [#4523](https://github.com/jupyterhub/jupyterhub/pull/4523) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- document how to use notebook v7 with jupyterhub [#4522](https://github.com/jupyterhub/jupyterhub/pull/4522) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2023-06-08&to=2023-08-10&type=c))
|
||||
|
||||
@agelosnm ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aagelosnm+updated%3A2023-06-08..2023-08-10&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @diocas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adiocas+updated%3A2023-06-08..2023-08-10&type=Issues)) | @grios-stratio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agrios-stratio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jhgoebbert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajhgoebbert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @jtpio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ajtpio+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kosmonavtus ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akosmonavtus+updated%3A2023-06-08..2023-08-10&type=Issues)) | @kreuzert ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Akreuzert+updated%3A2023-06-08..2023-08-10&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2023-06-08..2023-08-10&type=Issues)) | @martinRenou ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AmartinRenou+updated%3A2023-06-08..2023-08-10&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2023-06-08..2023-08-10&type=Issues)) | @opoplawski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aopoplawski+updated%3A2023-06-08..2023-08-10&type=Issues)) | @Ph0tonic ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3APh0tonic+updated%3A2023-06-08..2023-08-10&type=Issues)) | @sgaist ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgaist+updated%3A2023-06-08..2023-08-10&type=Issues)) | @trungleduc ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atrungleduc+updated%3A2023-06-08..2023-08-10&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2023-06-08..2023-08-10&type=Issues))
|
||||
|
||||
### 4.0.1 - 2023-06-08
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.0.0...4.0.1))
|
||||
|
||||
#### Enhancements made
|
||||
|
||||
- Delete server button on admin page [#4457](https://github.com/jupyterhub/jupyterhub/pull/4457) ([@diocas](https://github.com/diocas), [@minrk](https://github.com/minrk))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- Abort informatively on unrecognized CLI options [#4467](https://github.com/jupyterhub/jupyterhub/pull/4467) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add xsrf to custom_html template context [#4464](https://github.com/jupyterhub/jupyterhub/pull/4464) ([@opoplawski](https://github.com/opoplawski), [@minrk](https://github.com/minrk))
|
||||
- preserve CLI > env priority config in jupyterhub-singleuser extension [#4451](https://github.com/jupyterhub/jupyterhub/pull/4451) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@timeu](https://github.com/timeu), [@rcthomas](https://github.com/rcthomas))
|
||||
|
||||
#### Maintenance and upkeep improvements
|
||||
|
||||
- Fix link to collaboration accounts doc in example [#4448](https://github.com/jupyterhub/jupyterhub/pull/4448) ([@minrk](https://github.com/minrk))
|
||||
- Remove Dockerfile.alpine [#4444](https://github.com/jupyterhub/jupyterhub/pull/4444) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk))
|
||||
- Update jsx dependencies as much as possible [#4443](https://github.com/jupyterhub/jupyterhub/pull/4443) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Remove unused admin JS code [#4438](https://github.com/jupyterhub/jupyterhub/pull/4438) ([@yuvipanda](https://github.com/yuvipanda), [@minrk](https://github.com/minrk))
|
||||
- Finish migrating browser tests from selenium to playwright [#4435](https://github.com/jupyterhub/jupyterhub/pull/4435) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Migrate some tests from selenium to playwright [#4431](https://github.com/jupyterhub/jupyterhub/pull/4431) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
||||
- Begin setup of playwright tests [#4420](https://github.com/jupyterhub/jupyterhub/pull/4420) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- Reorder token request docs [#4463](https://github.com/jupyterhub/jupyterhub/pull/4463) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- 'servers' should be a dict of dicts, not a list of dicts in rest-api.yml [#4458](https://github.com/jupyterhub/jupyterhub/pull/4458) ([@tfmark](https://github.com/tfmark), [@minrk](https://github.com/minrk))
|
||||
- Config reference: link to nicer(?) API docs first [#4456](https://github.com/jupyterhub/jupyterhub/pull/4456) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add CERN to Gallery of JupyterHub Deployments [#4454](https://github.com/jupyterhub/jupyterhub/pull/4454) ([@goseind](https://github.com/goseind), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Fix "Thanks" typo. [#4441](https://github.com/jupyterhub/jupyterhub/pull/4441) ([@ryanlovett](https://github.com/ryanlovett), [@minrk](https://github.com/minrk))
|
||||
- add HUNT into research institutions [#4432](https://github.com/jupyterhub/jupyterhub/pull/4432) ([@matuskosut](https://github.com/matuskosut), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- docs: fix missing redirects for api to reference/api [#4429](https://github.com/jupyterhub/jupyterhub/pull/4429) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- update sharing faq for 2023 [#4428](https://github.com/jupyterhub/jupyterhub/pull/4428) ([@minrk](https://github.com/minrk))
|
||||
- Fix some public URL links within the docs [#4427](https://github.com/jupyterhub/jupyterhub/pull/4427) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- add upgrade note for 4.0 to changelog [#4426](https://github.com/jupyterhub/jupyterhub/pull/4426) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2023-04-20&to=2023-06-07&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2023-04-20..2023-06-07&type=Issues)) | @diocas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adiocas+updated%3A2023-04-20..2023-06-07&type=Issues)) | @echarles ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aecharles+updated%3A2023-04-20..2023-06-07&type=Issues)) | @goseind ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Agoseind+updated%3A2023-04-20..2023-06-07&type=Issues)) | @hsadia538 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ahsadia538+updated%3A2023-04-20..2023-06-07&type=Issues)) | @mahamtariq58 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amahamtariq58+updated%3A2023-04-20..2023-06-07&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2023-04-20..2023-06-07&type=Issues)) | @matuskosut ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amatuskosut+updated%3A2023-04-20..2023-06-07&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2023-04-20..2023-06-07&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2023-04-20..2023-06-07&type=Issues)) | @opoplawski ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aopoplawski+updated%3A2023-04-20..2023-06-07&type=Issues)) | @rcthomas ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Arcthomas+updated%3A2023-04-20..2023-06-07&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2023-04-20..2023-06-07&type=Issues)) | @tfmark ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atfmark+updated%3A2023-04-20..2023-06-07&type=Issues)) | @timeu ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Atimeu+updated%3A2023-04-20..2023-06-07&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2023-04-20..2023-06-07&type=Issues))
|
||||
|
||||
### 4.0.0 - 2023-04-20
|
||||
|
||||
4.0 is a major release, but a small one.
|
||||
|
||||
:::{admonition} Upgrade note
|
||||
|
||||
Upgrading from 3.1 to 4.0 should require no additional action beyond running `jupyterhub --upgrade-db` to upgrade the database schema after upgrading the package version.
|
||||
It is otherwise a regular jupyterhub [upgrade](upgrading-jupyterhub).
|
||||
:::
|
||||
|
||||
There are three major changes that _should_ be invisible to most users:
|
||||
|
||||
1. Groups can now have 'properties', editable via the admin page, which can be used by Spawners for their operations.
|
||||
@@ -24,7 +107,7 @@ There are three major changes that _should_ be invisible to most users:
|
||||
|
||||
In addition to these, thanks to contributions from this years Outreachy interns, we have reorganized the documentation according to [diataxis](https://diataxis.fr), improved accessibility of JupyterHub pages, and improved testing.
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/3.1.0...HEAD))
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/3.1.0...4.0.0))
|
||||
|
||||
#### API and Breaking Changes
|
||||
|
||||
@@ -33,18 +116,23 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
||||
|
||||
#### New features added
|
||||
|
||||
- add Spawner.server_token_scopes config [#4400](https://github.com/jupyterhub/jupyterhub/pull/4400) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Make singleuser server-extension default [#4354](https://github.com/jupyterhub/jupyterhub/pull/4354) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- singleuser auth as server extension [#3888](https://github.com/jupyterhub/jupyterhub/pull/3888) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Dynamic table for changing customizable properties of groups [#3651](https://github.com/jupyterhub/jupyterhub/pull/3651) ([@vladfreeze](https://github.com/vladfreeze), [@minrk](https://github.com/minrk), [@naatebarber](https://github.com/naatebarber), [@manics](https://github.com/manics))
|
||||
|
||||
#### Enhancements made
|
||||
|
||||
- admin page: improve display of long lists (groups, etc.) [#4417](https://github.com/jupyterhub/jupyterhub/pull/4417) ([@manics](https://github.com/manics), [@minrk](https://github.com/minrk), [@ryanlovett](https://github.com/ryanlovett))
|
||||
- add a few more buckets for server_spawn_duration_seconds [#4352](https://github.com/jupyterhub/jupyterhub/pull/4352) ([@shaneknapp](https://github.com/shaneknapp), [@yuvipanda](https://github.com/yuvipanda))
|
||||
- Standardize styling on input fields by moving common form CSS to page.less [#4294](https://github.com/jupyterhub/jupyterhub/pull/4294) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Improve contrast on muted text [#4326](https://github.com/jupyterhub/jupyterhub/pull/4326) ([@bl-aire](https://github.com/bl-aire), [@minrk](https://github.com/minrk))
|
||||
- Standardize styling on input fields by moving common form CSS to page.less [#4294](https://github.com/jupyterhub/jupyterhub/pull/4294) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- make sure named server URLs include trailing slash [#4402](https://github.com/jupyterhub/jupyterhub/pull/4402) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- fix inclusion of singleuser/templates/page.html in wheel [#4387](https://github.com/jupyterhub/jupyterhub/pull/4387) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
||||
- exponential_backoff: preserve jitter when max_wait is reached [#4383](https://github.com/jupyterhub/jupyterhub/pull/4383) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- admin panel: fix condition for start/stop buttons on user servers [#4365](https://github.com/jupyterhub/jupyterhub/pull/4365) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- avoid logging error when browsers send invalid cookies [#4356](https://github.com/jupyterhub/jupyterhub/pull/4356) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio))
|
||||
- test and fix deprecated load_groups list [#4299](https://github.com/jupyterhub/jupyterhub/pull/4299) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
@@ -55,6 +143,16 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
||||
|
||||
#### Maintenance and upkeep improvements
|
||||
|
||||
- add remaining redirects for docs reorg [#4423](https://github.com/jupyterhub/jupyterhub/pull/4423) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Disable dev traitlets [#4419](https://github.com/jupyterhub/jupyterhub/pull/4419) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio))
|
||||
- dependabot: rename to .yaml [#4409](https://github.com/jupyterhub/jupyterhub/pull/4409) ([@consideRatio](https://github.com/consideRatio))
|
||||
- dependabot: fix syntax error of not using quotes for ##:## [#4408](https://github.com/jupyterhub/jupyterhub/pull/4408) ([@consideRatio](https://github.com/consideRatio))
|
||||
- dependabot: monthly updates of github actions [#4403](https://github.com/jupyterhub/jupyterhub/pull/4403) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
|
||||
- Refresh 4.0 changelog [#4396](https://github.com/jupyterhub/jupyterhub/pull/4396) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Reduce size of jupyterhub image [#4394](https://github.com/jupyterhub/jupyterhub/pull/4394) ([@alekseyolg](https://github.com/alekseyolg), [@minrk](https://github.com/minrk))
|
||||
- Selenium: updating test_oauth_page [#4393](https://github.com/jupyterhub/jupyterhub/pull/4393) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
||||
- avoid warning on engine_connect listener [#4392](https://github.com/jupyterhub/jupyterhub/pull/4392) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@manics](https://github.com/manics))
|
||||
- remove pin from singleuser [#4379](https://github.com/jupyterhub/jupyterhub/pull/4379) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@mathbunnyru](https://github.com/mathbunnyru))
|
||||
- temporary fix: pin base-notebook tag [#4348](https://github.com/jupyterhub/jupyterhub/pull/4348) ([@minrk](https://github.com/minrk))
|
||||
- simplify some async fixtures [#4332](https://github.com/jupyterhub/jupyterhub/pull/4332) ([@minrk](https://github.com/minrk), [@GeorgianaElena](https://github.com/GeorgianaElena), [@Sheila-nk](https://github.com/Sheila-nk))
|
||||
- Selenium: adding new cases that covered Admin UI page [#4328](https://github.com/jupyterhub/jupyterhub/pull/4328) ([@mouse1203](https://github.com/mouse1203), [@minrk](https://github.com/minrk))
|
||||
@@ -76,9 +174,19 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
||||
|
||||
#### Documentation improvements
|
||||
|
||||
- Fix variable spelling. [#4398](https://github.com/jupyterhub/jupyterhub/pull/4398) ([@ryanlovett](https://github.com/ryanlovett), [@manics](https://github.com/manics))
|
||||
- Remove bracket around link text without address [#4416](https://github.com/jupyterhub/jupyterhub/pull/4416) ([@crazytan](https://github.com/crazytan), [@minrk](https://github.com/minrk))
|
||||
- add some more detail and examples to database doc [#4399](https://github.com/jupyterhub/jupyterhub/pull/4399) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Add emphasis about role loading and hub restarts. [#4390](https://github.com/jupyterhub/jupyterhub/pull/4390) ([@ryanlovett](https://github.com/ryanlovett), [@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Re-enable links to REST API [#4386](https://github.com/jupyterhub/jupyterhub/pull/4386) ([@minrk](https://github.com/minrk), [@manics](https://github.com/manics))
|
||||
- reduce nested hierarchy in docs organization [#4377](https://github.com/jupyterhub/jupyterhub/pull/4377) ([@alwasega](https://github.com/alwasega), [@minrk](https://github.com/minrk))
|
||||
- changelog for 4.0 beta [#4375](https://github.com/jupyterhub/jupyterhub/pull/4375) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Getting started link broken [#4374](https://github.com/jupyterhub/jupyterhub/pull/4374) ([@3coins](https://github.com/3coins), [@minrk](https://github.com/minrk))
|
||||
- add collaboration accounts tutorial [#4373](https://github.com/jupyterhub/jupyterhub/pull/4373) ([@minrk](https://github.com/minrk), [@fperez](https://github.com/fperez), [@ryanlovett](https://github.com/ryanlovett))
|
||||
- Updated the top-level index file [#4368](https://github.com/jupyterhub/jupyterhub/pull/4368) ([@alwasega](https://github.com/alwasega), [@sgibson91](https://github.com/sgibson91))
|
||||
- JupyterHub sphinx theme [#4363](https://github.com/jupyterhub/jupyterhub/pull/4363) ([@minrk](https://github.com/minrk), [@choldgraf](https://github.com/choldgraf))
|
||||
- Remove PDF links from README.md [#4358](https://github.com/jupyterhub/jupyterhub/pull/4358) ([@pnasrat](https://github.com/pnasrat), [@manics](https://github.com/manics))
|
||||
- add singleuser explanation doc [#4357](https://github.com/jupyterhub/jupyterhub/pull/4357) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
- Updates to the documentation Contribution section [#4355](https://github.com/jupyterhub/jupyterhub/pull/4355) ([@alwasega](https://github.com/alwasega), [@sgibson91](https://github.com/sgibson91), [@minrk](https://github.com/minrk))
|
||||
- Restructured references section of the docs [#4343](https://github.com/jupyterhub/jupyterhub/pull/4343) ([@alwasega](https://github.com/alwasega), [@minrk](https://github.com/minrk), [@sgibson91](https://github.com/sgibson91))
|
||||
- Document use of pytest-asyncio in JupyterHub test suite [#4341](https://github.com/jupyterhub/jupyterhub/pull/4341) ([@Sheila-nk](https://github.com/Sheila-nk), [@minrk](https://github.com/minrk), [@alwasega](https://github.com/alwasega))
|
||||
@@ -107,9 +215,9 @@ In addition to these, thanks to contributions from this years Outreachy interns,
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-12-05&to=2023-02-27&type=c))
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-12-05&to=2023-04-20&type=c))
|
||||
|
||||
@3coins ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A3coins+updated%3A2022-12-05..2023-02-27&type=Issues)) | @ajcollett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajcollett+updated%3A2022-12-05..2023-02-27&type=Issues)) | @ajpower ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajpower+updated%3A2022-12-05..2023-02-27&type=Issues)) | @alwasega ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalwasega+updated%3A2022-12-05..2023-02-27&type=Issues)) | @betatim ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2022-12-05..2023-02-27&type=Issues)) | @bl-aire ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abl-aire+updated%3A2022-12-05..2023-02-27&type=Issues)) | @choldgraf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2022-12-05..2023-02-27&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2022-12-05..2023-02-27&type=Issues)) | @dependabot ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adependabot+updated%3A2022-12-05..2023-02-27&type=Issues)) | @GeorgianaElena ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AGeorgianaElena+updated%3A2022-12-05..2023-02-27&type=Issues)) | @julietKiloRomeo ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AjulietKiloRomeo+updated%3A2022-12-05..2023-02-27&type=Issues)) | @ktaletsk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aktaletsk+updated%3A2022-12-05..2023-02-27&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2022-12-05..2023-02-27&type=Issues)) | @meeseeksdev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksdev+updated%3A2022-12-05..2023-02-27&type=Issues)) | @meeseeksmachine ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2022-12-05..2023-02-27&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2022-12-05..2023-02-27&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2022-12-05..2023-02-27&type=Issues)) | @naatebarber ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2022-12-05..2023-02-27&type=Issues)) | @pnasrat ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apnasrat+updated%3A2022-12-05..2023-02-27&type=Issues)) | @pre-commit-ci ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apre-commit-ci+updated%3A2022-12-05..2023-02-27&type=Issues)) | @sgibson91 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgibson91+updated%3A2022-12-05..2023-02-27&type=Issues)) | @shaneknapp ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ashaneknapp+updated%3A2022-12-05..2023-02-27&type=Issues)) | @Sheila-nk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ASheila-nk+updated%3A2022-12-05..2023-02-27&type=Issues)) | @stevejpurves ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Astevejpurves+updated%3A2022-12-05..2023-02-27&type=Issues)) | @TaofeeqatDev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ATaofeeqatDev+updated%3A2022-12-05..2023-02-27&type=Issues)) | @vladfreeze ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Avladfreeze+updated%3A2022-12-05..2023-02-27&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2022-12-05..2023-02-27&type=Issues))
|
||||
@3coins ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A3coins+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ajcollett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajcollett+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ajpower ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aajpower+updated%3A2022-12-05..2023-04-20&type=Issues)) | @alekseyolg ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalekseyolg+updated%3A2022-12-05..2023-04-20&type=Issues)) | @alwasega ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aalwasega+updated%3A2022-12-05..2023-04-20&type=Issues)) | @betatim ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2022-12-05..2023-04-20&type=Issues)) | @bl-aire ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abl-aire+updated%3A2022-12-05..2023-04-20&type=Issues)) | @choldgraf ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2022-12-05..2023-04-20&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2022-12-05..2023-04-20&type=Issues)) | @crazytan ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acrazytan+updated%3A2022-12-05..2023-04-20&type=Issues)) | @dependabot ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adependabot+updated%3A2022-12-05..2023-04-20&type=Issues)) | @fperez ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Afperez+updated%3A2022-12-05..2023-04-20&type=Issues)) | @GeorgianaElena ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AGeorgianaElena+updated%3A2022-12-05..2023-04-20&type=Issues)) | @julietKiloRomeo ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AjulietKiloRomeo+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ktaletsk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aktaletsk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2022-12-05..2023-04-20&type=Issues)) | @mathbunnyru ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amathbunnyru+updated%3A2022-12-05..2023-04-20&type=Issues)) | @meeseeksdev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksdev+updated%3A2022-12-05..2023-04-20&type=Issues)) | @meeseeksmachine ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2022-12-05..2023-04-20&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @mouse1203 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amouse1203+updated%3A2022-12-05..2023-04-20&type=Issues)) | @naatebarber ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2022-12-05..2023-04-20&type=Issues)) | @pnasrat ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apnasrat+updated%3A2022-12-05..2023-04-20&type=Issues)) | @pre-commit-ci ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Apre-commit-ci+updated%3A2022-12-05..2023-04-20&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aryanlovett+updated%3A2022-12-05..2023-04-20&type=Issues)) | @sgibson91 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asgibson91+updated%3A2022-12-05..2023-04-20&type=Issues)) | @shaneknapp ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ashaneknapp+updated%3A2022-12-05..2023-04-20&type=Issues)) | @Sheila-nk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ASheila-nk+updated%3A2022-12-05..2023-04-20&type=Issues)) | @stevejpurves ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Astevejpurves+updated%3A2022-12-05..2023-04-20&type=Issues)) | @TaofeeqatDev ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3ATaofeeqatDev+updated%3A2022-12-05..2023-04-20&type=Issues)) | @vladfreeze ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Avladfreeze+updated%3A2022-12-05..2023-04-20&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2022-12-05..2023-04-20&type=Issues))
|
||||
|
||||
## 3.1
|
||||
|
||||
@@ -1345,7 +1453,7 @@ Thanks to everyone who has contributed to this release!
|
||||
- `JupyterHub.init_spawners_timeout` is introduced to combat slow startups on large JupyterHub deployments [#2721](https://github.com/jupyterhub/jupyterhub/pull/2721) ([@minrk](https://github.com/minrk))
|
||||
- The configuration `uids` for local authenticators is added to consistently assign users UNIX id's between installations [#2687](https://github.com/jupyterhub/jupyterhub/pull/2687) ([@rgerkin](https://github.com/rgerkin))
|
||||
- `JupyterHub.activity_resolution` is introduced with a default value of 30s improving performance by not updating the database with user activity too often [#2605](https://github.com/jupyterhub/jupyterhub/pull/2605) ([@minrk](https://github.com/minrk))
|
||||
- [HubAuth](https://jupyterhub.readthedocs.io/en/stable/api/services.auth.html#jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
|
||||
- [HubAuth](jupyterhub.services.auth.HubAuth)'s SSL configuration can now be set through environment variables [#2588](https://github.com/jupyterhub/jupyterhub/pull/2588) ([@cmd-ntrf](https://github.com/cmd-ntrf))
|
||||
- Expose spawner.user_options in REST API. [#2755](https://github.com/jupyterhub/jupyterhub/pull/2755) ([@danielballan](https://github.com/danielballan))
|
||||
- add block for scripts included in head [#2828](https://github.com/jupyterhub/jupyterhub/pull/2828) ([@bitnik](https://github.com/bitnik))
|
||||
- Instrument JupyterHub to record events with jupyter_telemetry [Part II] [#2698](https://github.com/jupyterhub/jupyterhub/pull/2698) ([@Zsailer](https://github.com/Zsailer))
|
||||
|
@@ -14,6 +14,12 @@ section, the `jupyterhub_config.py` can be automatically generated via
|
||||
> jupyterhub --generate-config
|
||||
> ```
|
||||
|
||||
Most of this information is available in a nicer format in:
|
||||
|
||||
- [](./api/app.md)
|
||||
- [](./api/auth.md)
|
||||
- [](./api/spawner.md)
|
||||
|
||||
The following contains the output of that command for reference.
|
||||
|
||||
```{eval-rst}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Event logging and telemetry
|
||||
|
||||
JupyterHub can be configured to record structured events from a running server using Jupyter's [Telemetry System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this [page].
|
||||
JupyterHub can be configured to record structured events from a running server using Jupyter's [Telemetry System]. The types of events that JupyterHub emits are defined by [JSON schemas] listed at the bottom of this page.
|
||||
|
||||
## How to emit events
|
||||
|
||||
|
@@ -63,6 +63,15 @@ easy to do with RStudio too.
|
||||
|
||||
- [jupyterhub-deploy-teaching](https://github.com/jupyterhub/jupyterhub-deploy-teaching) based on work by Brian Granger for Cal Poly's Data Science 301 Course
|
||||
|
||||
### CERN
|
||||
|
||||
[CERN](https://home.cern/), also known as the European Organization for Nuclear Research, is a world-renowned scientific research centre and the home of the Large Hadron Collider (LHC).
|
||||
|
||||
Within CERN, there are two noteworthy JupyterHub deployments in operation:
|
||||
|
||||
- [SWAN](https://swan.web.cern.ch/swan/), which stands for Service for Web based Analysis, serves as an interactive data analysis platform primarily utilized at CERN.
|
||||
- [VRE](https://vre-hub.github.io/), which stands for Virtual Research Environment, is an analysis platform developed within the [EOSC Project](https://eoscfuture.eu/) to cater to the needs of scientific communities involved in European projects.
|
||||
|
||||
### Chameleon
|
||||
|
||||
[Chameleon](https://www.chameleoncloud.org) is a NSF-funded configurable experimental environment for large-scale computer science systems research with [bare metal reconfigurability](https://chameleoncloud.readthedocs.io/en/latest/technical/baremetal.html). Chameleon users utilize JupyterHub to document and reproduce their complex CISE and networking experiments.
|
||||
@@ -83,7 +92,6 @@ easy to do with RStudio too.
|
||||
- Slurm job dispatched on Crestone compute cluster
|
||||
- log troubleshooting
|
||||
- Profiles in IPython Clusters tab
|
||||
- [Parallel Processing with JupyterHub tutorial](https://curc.readthedocs.io/en/latest/gateways/parallel-programming-jupyter.html)
|
||||
|
||||
### George Washington University
|
||||
|
||||
|
@@ -124,6 +124,8 @@ for project_name, project in project_config["projects"].items():
|
||||
|
||||
The `members` step could be skipped if group membership is managed by the authenticator, or handled via the admin UI later, in which case we only need to handle group _creation_ and role assignment.
|
||||
|
||||
This configuration code runs when jupyterhub starts up, and as noted above, users and groups cannot have their role assignments change without restarting JupyterHub. If new collaboration groups are created (within configuration, via the admin page, or via the Authenticator), the hub will need to be restarted in order for it to load roles for those new groups.
|
||||
|
||||
### Distinguishing collaborative servers
|
||||
|
||||
Finally, we want to enable RTC only on the collaborative user servers (and _only_ the collaborative user servers),
|
||||
|
@@ -39,7 +39,7 @@ openssl rand -hex 32
|
||||
In [version 0.8.0](changelog), a TOKEN request page for
|
||||
generating an API token is available from the JupyterHub user interface:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
|
@@ -2,4 +2,4 @@
|
||||
|
||||
An example of enabling real-time collaboration with dedicated accounts for collaborations.
|
||||
|
||||
See [collaboration account docs](docs/source/tutorial/collaboration-accounts.md) for details.
|
||||
See [collaboration account docs](../../docs/source/tutorial/collaboration-users.md) for details.
|
||||
|
@@ -25,51 +25,49 @@
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "identity-obj-proxy"
|
||||
}
|
||||
},
|
||||
"testEnvironment": "jsdom"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.3",
|
||||
"history": "^5.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-bootstrap": "^2.1.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-icons": "^4.1.0",
|
||||
"react-multi-select-component": "^3.0.7",
|
||||
"react-object-table-viewer": "^1.0.7",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"recompose": "npm:react-recompose@^0.31.2",
|
||||
"redux": "^4.0.5",
|
||||
"regenerator-runtime": "^0.13.9"
|
||||
"bootstrap": "^5.2.3",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-multi-select-component": "^4.3.4",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"recompose": "npm:react-recompose@^0.33.0",
|
||||
"redux": "^4.2.1",
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@testing-library/jest-dom": "^5.15.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@webpack-cli/serve": "^1.7.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-loader": "^8.2.1",
|
||||
"css-loader": "^5.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.1",
|
||||
"@webpack-cli/serve": "^2.0.1",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.2.1",
|
||||
"sinon": "^13.0.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"webpack": "^5.6.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.3"
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.7",
|
||||
"sinon": "^15.0.3",
|
||||
"style-loader": "^3.3.2",
|
||||
"webpack": "^5.79.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
}
|
||||
}
|
||||
|
21
jsx/src/components/ReactObjectTableViewer/LICENSE
Normal file
21
jsx/src/components/ReactObjectTableViewer/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 jinkwon.lee<uzmystic@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@@ -0,0 +1,65 @@
|
||||
// Originally copied from
|
||||
// https://github.com/jinkwon/react-object-table-viewer/blob/f29827028fad547a0a17e044567cf1486849fb7a/src/ReactObjectTableViewer.tsx
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ReactObjectTableViewer = (props) => {
|
||||
const opt = props;
|
||||
|
||||
const data = opt.data;
|
||||
const keys = Object.keys(data || {}) || [];
|
||||
|
||||
return (
|
||||
<table
|
||||
className={opt.className}
|
||||
style={{
|
||||
...opt.style,
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{keys.map((k, key) => {
|
||||
const val = data[k];
|
||||
const isObject = typeof val === "object";
|
||||
const isElement = React.isValidElement(val);
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<th
|
||||
style={{
|
||||
...opt.keyStyle,
|
||||
}}
|
||||
>
|
||||
{k}
|
||||
</th>
|
||||
{isObject && (
|
||||
<td>
|
||||
{isElement && val}
|
||||
{!isElement && <ReactObjectTableViewer {...opt} data={val} />}
|
||||
</td>
|
||||
)}
|
||||
{!isObject && (
|
||||
<td
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
...opt.valueStyle,
|
||||
}}
|
||||
>{`${val}`}</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
ReactObjectTableViewer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
style: PropTypes.objectOf(PropTypes.string),
|
||||
keyStyle: PropTypes.objectOf(PropTypes.string),
|
||||
valueStyle: PropTypes.objectOf(PropTypes.string),
|
||||
className: PropTypes.string,
|
||||
layout: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ReactObjectTableViewer;
|
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, Fragment } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { debounce } from "lodash";
|
||||
import PropTypes from "prop-types";
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CardGroup,
|
||||
Collapse,
|
||||
} from "react-bootstrap";
|
||||
import ReactObjectTableViewer from "react-object-table-viewer";
|
||||
import ReactObjectTableViewer from "../ReactObjectTableViewer/ReactObjectTableViewer";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
|
||||
@@ -29,6 +29,13 @@ const AccessServerButton = ({ url }) => (
|
||||
</a>
|
||||
);
|
||||
|
||||
const RowListItem = ({ text }) => (
|
||||
<span className="server-dashboard-row-list-item">{text}</span>
|
||||
);
|
||||
RowListItem.propTypes = {
|
||||
text: PropTypes.string,
|
||||
};
|
||||
|
||||
const ServerDashboard = (props) => {
|
||||
let base_url = window.base_url || "/";
|
||||
// sort methods
|
||||
@@ -67,6 +74,7 @@ const ServerDashboard = (props) => {
|
||||
shutdownHub,
|
||||
startServer,
|
||||
stopServer,
|
||||
deleteServer,
|
||||
startAll,
|
||||
stopAll,
|
||||
history,
|
||||
@@ -160,6 +168,50 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteServerButton = ({ serverName, userName }) => {
|
||||
if (serverName === "") {
|
||||
return null;
|
||||
}
|
||||
var [isDisabled, setIsDisabled] = useState(false);
|
||||
return (
|
||||
<button
|
||||
className="btn btn-danger btn-xs stop-button"
|
||||
// It's not possible to delete unnamed servers
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
setIsDisabled(true);
|
||||
deleteServer(userName, serverName)
|
||||
.then((res) => {
|
||||
if (res.status < 300) {
|
||||
updateUsers(...slice)
|
||||
.then((data) => {
|
||||
dispatchPageUpdate(
|
||||
data.items,
|
||||
data._pagination,
|
||||
name_filter,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsDisabled(false);
|
||||
setErrorAlert(`Failed to update users list.`);
|
||||
});
|
||||
} else {
|
||||
setErrorAlert(`Failed to delete server.`);
|
||||
setIsDisabled(false);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorAlert(`Failed to delete server.`);
|
||||
setIsDisabled(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete Server
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const StartServerButton = ({ serverName, userName }) => {
|
||||
var [isDisabled, setIsDisabled] = useState(false);
|
||||
return (
|
||||
@@ -236,8 +288,13 @@ const ServerDashboard = (props) => {
|
||||
break;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
// cast arrays (e.g. roles, groups) to string
|
||||
value = value.sort().join(", ");
|
||||
value = (
|
||||
<Fragment>
|
||||
{value.sort().flatMap((v) => (
|
||||
<RowListItem text={v} />
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
result[key] = value;
|
||||
return result;
|
||||
@@ -266,7 +323,11 @@ const ServerDashboard = (props) => {
|
||||
const userServerName = user.name + serverNameDash;
|
||||
const open = collapseStates[userServerName] || false;
|
||||
return [
|
||||
<tr key={`${userServerName}-row`} className="user-row">
|
||||
<tr
|
||||
key={`${userServerName}-row`}
|
||||
data-testid={`user-row-${userServerName}`}
|
||||
className="user-row"
|
||||
>
|
||||
<td data-testid="user-row-name">
|
||||
<span>
|
||||
<Button
|
||||
@@ -312,6 +373,10 @@ const ServerDashboard = (props) => {
|
||||
userName={user.name}
|
||||
style={{ marginRight: 20 }}
|
||||
/>
|
||||
<DeleteServerButton
|
||||
serverName={server.name}
|
||||
userName={user.name}
|
||||
/>
|
||||
<a
|
||||
href={`${base_url}spawn/${user.name}${
|
||||
server.name ? "/" + server.name : ""
|
||||
@@ -570,6 +635,7 @@ ServerDashboard.propTypes = {
|
||||
shutdownHub: PropTypes.func,
|
||||
startServer: PropTypes.func,
|
||||
stopServer: PropTypes.func,
|
||||
deleteServer: PropTypes.func,
|
||||
startAll: PropTypes.func,
|
||||
stopAll: PropTypes.func,
|
||||
dispatch: PropTypes.func,
|
||||
|
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, getByText } from "@testing-library/react";
|
||||
import { HashRouter, Switch } from "react-router-dom";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
import { createStore } from "redux";
|
||||
@@ -43,6 +43,31 @@ var mockAsync = (data) =>
|
||||
var mockAsyncRejection = () =>
|
||||
jest.fn().mockImplementation(() => Promise.reject());
|
||||
|
||||
var bar_servers = {
|
||||
"": {
|
||||
name: "",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/progress",
|
||||
},
|
||||
servername: {
|
||||
name: "servername",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/servername",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/servername/progress",
|
||||
},
|
||||
};
|
||||
|
||||
var mockAppState = () =>
|
||||
Object.assign({}, initialState, {
|
||||
user_data: [
|
||||
@@ -78,19 +103,7 @@ var mockAppState = () =>
|
||||
pending: null,
|
||||
created: "2020-12-07T18:46:27.115528Z",
|
||||
last_activity: "2020-12-07T20:43:51.013613Z",
|
||||
servers: {
|
||||
"": {
|
||||
name: "",
|
||||
last_activity: "2020-12-07T20:58:02.437408Z",
|
||||
started: "2020-12-07T20:58:01.508266Z",
|
||||
pending: null,
|
||||
ready: false,
|
||||
state: { pid: 12345 },
|
||||
url: "/user/bar/",
|
||||
user_options: {},
|
||||
progress_url: "/hub/api/users/bar/progress",
|
||||
},
|
||||
},
|
||||
servers: bar_servers,
|
||||
},
|
||||
],
|
||||
user_page: {
|
||||
@@ -150,9 +163,11 @@ test("Renders users from props.user_data into table", async () => {
|
||||
|
||||
let foo = screen.getByTestId("user-name-div-foo");
|
||||
let bar = screen.getByTestId("user-name-div-bar");
|
||||
let bar_server = screen.getByTestId("user-name-div-bar-servername");
|
||||
|
||||
expect(foo).toBeVisible();
|
||||
expect(bar).toBeVisible();
|
||||
expect(bar_server).toBeVisible();
|
||||
});
|
||||
|
||||
test("Renders correctly the status of a single-user server", async () => {
|
||||
@@ -162,10 +177,13 @@ test("Renders correctly the status of a single-user server", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let stop = screen.getByText("Stop Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
start_elems.forEach((start) => {
|
||||
expect(start).toBeVisible();
|
||||
});
|
||||
|
||||
expect(start).toBeVisible();
|
||||
let stop = screen.getByText("Stop Server");
|
||||
expect(stop).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -176,9 +194,12 @@ test("Renders spawn page link", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let link = screen.getByText("Spawn Page").closest("a");
|
||||
let url = new URL(link.href);
|
||||
expect(url.pathname).toEqual("/spawn/bar");
|
||||
for (let server in bar_servers) {
|
||||
let row = screen.getByTestId(`user-row-bar${server ? "-" + server : ""}`);
|
||||
let link = getByText(row, "Spawn Page").closest("a");
|
||||
let url = new URL(link.href);
|
||||
expect(url.pathname).toEqual("/spawn/bar" + (server ? "/" + server : ""));
|
||||
}
|
||||
});
|
||||
|
||||
test("Invokes the startServer event on button click", async () => {
|
||||
@@ -188,10 +209,11 @@ test("Invokes the startServer event on button click", async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
expect(callbackSpy).toHaveBeenCalled();
|
||||
@@ -453,10 +475,11 @@ test("Shows a UI error dialogue when start user server fails", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
let errorDialog = screen.getByText("Failed to start server.");
|
||||
@@ -487,10 +510,11 @@ test("Shows a UI error dialogue when start user server returns an improper statu
|
||||
);
|
||||
});
|
||||
|
||||
let start = screen.getByText("Start Server");
|
||||
let start_elems = screen.getAllByText("Start Server");
|
||||
expect(start_elems.length).toBe(Object.keys(bar_servers).length);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(start);
|
||||
fireEvent.click(start_elems[0]);
|
||||
});
|
||||
|
||||
let errorDialog = screen.getByText("Failed to start server.");
|
||||
@@ -656,3 +680,20 @@ test("Interacting with PaginationFooter causes state update and refresh via useE
|
||||
// expect(callbackSpy.mock.calls).toHaveLength(2);
|
||||
// expect(callbackSpy).toHaveBeenCalledWith(2, 2, "");
|
||||
});
|
||||
|
||||
test("Server delete button exists for named servers", async () => {
|
||||
let callbackSpy = mockAsync();
|
||||
|
||||
await act(async () => {
|
||||
render(serverDashboardJsx(callbackSpy));
|
||||
});
|
||||
|
||||
for (let server in bar_servers) {
|
||||
if (server === "") {
|
||||
continue;
|
||||
}
|
||||
let row = screen.getByTestId(`user-row-bar-${server}`);
|
||||
let delete_button = getByText(row, "Delete Server");
|
||||
expect(delete_button).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
@@ -30,3 +30,11 @@
|
||||
tr.noborder > td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.server-dashboard-row-list-item {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
margin: 2px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
@@ -18,6 +18,12 @@ const withAPI = withProps(() => ({
|
||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "POST"),
|
||||
stopServer: (name, serverName = "") =>
|
||||
jhapiRequest("/users/" + name + "/servers/" + (serverName || ""), "DELETE"),
|
||||
deleteServer: (name, serverName = "") =>
|
||||
jhapiRequest(
|
||||
"/users/" + name + "/servers/" + (serverName || ""),
|
||||
"DELETE",
|
||||
{ remove: true },
|
||||
),
|
||||
startAll: (names) =>
|
||||
names.map((e) => jhapiRequest("/users/" + e + "/server", "POST")),
|
||||
stopAll: (names) =>
|
||||
|
17029
jsx/yarn.lock
17029
jsx/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (4, 0, 0, "b1", "")
|
||||
version_info = (4, 0, 2, "", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -89,6 +89,11 @@ class APIHandler(BaseHandler):
|
||||
if not hasattr(self, '_jupyterhub_user'):
|
||||
# called too early to check if we're token-authenticated
|
||||
return
|
||||
if self._jupyterhub_user is None and 'Origin' not in self.request.headers:
|
||||
# don't raise xsrf if auth failed
|
||||
# don't apply this shortcut to actual cross-site requests, which have an 'Origin' header,
|
||||
# which would reveal if there are credentials present
|
||||
return
|
||||
if getattr(self, '_token_authenticated', False):
|
||||
# if token-authenticated, ignore XSRF
|
||||
return
|
||||
|
@@ -9,6 +9,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
@@ -2840,6 +2841,10 @@ class JupyterHub(Application):
|
||||
super().initialize(*args, **kwargs)
|
||||
if self.generate_config or self.generate_certs or self.subapp:
|
||||
return
|
||||
if self.extra_args:
|
||||
self.exit(
|
||||
f"Unrecognized command-line arguments: {' '.join(shlex.quote(arg) for arg in self.extra_args)!r}"
|
||||
)
|
||||
self._start_future = asyncio.Future()
|
||||
|
||||
def record_start(f):
|
||||
|
@@ -236,11 +236,13 @@ class BaseHandler(RequestHandler):
|
||||
def check_xsrf_cookie(self):
|
||||
try:
|
||||
return super().check_xsrf_cookie()
|
||||
except Exception as e:
|
||||
# ensure _juptyerhub_user is defined on rejected requests
|
||||
except web.HTTPError as e:
|
||||
# ensure _jupyterhub_user is defined on rejected requests
|
||||
if not hasattr(self, "_jupyterhub_user"):
|
||||
self._jupyterhub_user = None
|
||||
self._resolve_roles_and_scopes()
|
||||
# rewrite message because we use this on methods other than POST
|
||||
e.log_message = e.log_message.replace("POST", self.request.method)
|
||||
raise
|
||||
|
||||
@property
|
||||
@@ -1431,6 +1433,12 @@ class UserUrlHandler(BaseHandler):
|
||||
# accept token auth for API requests that are probably to non-running servers
|
||||
_accept_token_auth = True
|
||||
|
||||
# don't consider these redirects 'activity'
|
||||
# if the redirect is followed and the subsequent action taken,
|
||||
# _that_ is activity
|
||||
def _record_activity(self, obj, timestamp=None):
|
||||
return False
|
||||
|
||||
def _fail_api_request(self, user_name='', server_name=''):
|
||||
"""Fail an API request to a not-running server"""
|
||||
self.log.warning(
|
||||
|
@@ -105,6 +105,7 @@ class LoginHandler(BaseHandler):
|
||||
'next': self.get_argument('next', ''),
|
||||
},
|
||||
),
|
||||
"xsrf": self.xsrf_token.decode('ascii'),
|
||||
}
|
||||
custom_html = Template(
|
||||
self.authenticator.get_custom_html(self.hub.base_url)
|
||||
|
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
import alembic.command
|
||||
import alembic.config
|
||||
import sqlalchemy
|
||||
from alembic.script import ScriptDirectory
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
@@ -902,10 +903,12 @@ def register_ping_connection(engine):
|
||||
https://docs.sqlalchemy.org/en/rel_1_1/core/pooling.html#disconnect-handling-pessimistic
|
||||
"""
|
||||
|
||||
@event.listens_for(engine, "engine_connect")
|
||||
def ping_connection(connection, branch=None):
|
||||
# TODO: remove unused branch arg when we require sqlalchemy 2.0
|
||||
|
||||
# listeners are normally registered as a decorator,
|
||||
# but we need two different signatures to avoid SAWarning:
|
||||
# The argument signature for the "ConnectionEvents.engine_connect" event listener has changed
|
||||
# while we support sqla 1.4 and 2.0.
|
||||
# @event.listens_for(engine, "engine_connect")
|
||||
def ping_connection(connection):
|
||||
# turn off "close with result". This flag is only used with
|
||||
# "connectionless" execution, otherwise will be False in any case
|
||||
save_should_close_with_result = connection.should_close_with_result
|
||||
@@ -939,6 +942,17 @@ def register_ping_connection(engine):
|
||||
# restore "close with result"
|
||||
connection.should_close_with_result = save_should_close_with_result
|
||||
|
||||
# sqla v1/v2 compatible invocation of @event.listens_for:
|
||||
def ping_connection_v1(connection, branch=None):
|
||||
"""sqlalchemy < 2.0 compatibility"""
|
||||
return ping_connection(connection)
|
||||
|
||||
if int(sqlalchemy.__version__.split(".", 1)[0]) >= 2:
|
||||
listener = ping_connection
|
||||
else:
|
||||
listener = ping_connection_v1
|
||||
event.listens_for(engine, "engine_connect")(listener)
|
||||
|
||||
|
||||
def check_db_revision(engine):
|
||||
"""Check the JupyterHub database revision
|
||||
|
@@ -52,7 +52,7 @@ def get_default_roles():
|
||||
'description': 'Post activity only',
|
||||
'scopes': [
|
||||
'users:activity!user',
|
||||
'access:servers!user',
|
||||
'access:servers!server',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -845,6 +845,15 @@ def needs_scope(*scopes):
|
||||
def scope_decorator(func):
|
||||
@functools.wraps(func)
|
||||
def _auth_func(self, *args, **kwargs):
|
||||
if not self.current_user:
|
||||
# not authenticated at all, fail with more generic message
|
||||
# this is the most likely permission error - missing or mis-specified credentials,
|
||||
# don't indicate that they have insufficient permissions.
|
||||
raise web.HTTPError(
|
||||
403,
|
||||
"Missing or invalid credentials.",
|
||||
)
|
||||
|
||||
sig = inspect.signature(func)
|
||||
bound_sig = sig.bind(self, *args, **kwargs)
|
||||
bound_sig.apply_defaults()
|
||||
@@ -853,6 +862,11 @@ def needs_scope(*scopes):
|
||||
self.expanded_scopes = {}
|
||||
self.parsed_scopes = {}
|
||||
|
||||
try:
|
||||
end_point = self.request.path
|
||||
except AttributeError:
|
||||
end_point = self.__name__
|
||||
|
||||
s_kwargs = {}
|
||||
for resource in {'user', 'server', 'group', 'service'}:
|
||||
resource_name = resource + '_name'
|
||||
@@ -860,14 +874,10 @@ def needs_scope(*scopes):
|
||||
resource_value = bound_sig.arguments[resource_name]
|
||||
s_kwargs[resource] = resource_value
|
||||
for scope in scopes:
|
||||
app_log.debug("Checking access via scope %s", scope)
|
||||
app_log.debug("Checking access to %s via scope %s", end_point, scope)
|
||||
has_access = _check_scope_access(self, scope, **s_kwargs)
|
||||
if has_access:
|
||||
return func(self, *args, **kwargs)
|
||||
try:
|
||||
end_point = self.request.path
|
||||
except AttributeError:
|
||||
end_point = self.__name__
|
||||
app_log.warning(
|
||||
"Not authorizing access to {}. Requires any of [{}], not derived from scopes [{}]".format(
|
||||
end_point, ", ".join(scopes), ", ".join(self.expanded_scopes)
|
||||
|
@@ -6,7 +6,7 @@
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
Default app changed to launch `jupyter labhub`.
|
||||
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
|
||||
Use JUPYTERHUB_SINGLEUSER_APP='notebook' for the legacy 'classic' notebook server (requires notebook<7).
|
||||
"""
|
||||
import os
|
||||
|
||||
@@ -27,7 +27,25 @@ JUPYTERHUB_SINGLEUSER_APP = _app_shortcuts.get(
|
||||
JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
|
||||
)
|
||||
|
||||
|
||||
if JUPYTERHUB_SINGLEUSER_APP:
|
||||
if JUPYTERHUB_SINGLEUSER_APP in {"notebook", _app_shortcuts["notebook"]}:
|
||||
# better error for notebook v7, which uses jupyter-server
|
||||
# when the legacy notebook server is requested
|
||||
try:
|
||||
from notebook import __version__
|
||||
except ImportError:
|
||||
# will raise later
|
||||
pass
|
||||
else:
|
||||
# check if this failed because of notebook v7
|
||||
_notebook_major_version = int(__version__.split(".", 1)[0])
|
||||
if _notebook_major_version >= 7:
|
||||
raise ImportError(
|
||||
f"JUPYTERHUB_SINGLEUSER_APP={JUPYTERHUB_SINGLEUSER_APP} is not valid with notebook>=7 (have notebook=={__version__}).\n"
|
||||
f"Leave $JUPYTERHUB_SINGLEUSER_APP unspecified (or use the default JUPYTERHUB_SINGLEUSER_APP=jupyter-server), "
|
||||
'and set `c.Spawner.default_url = "/tree"` to make notebook v7 the default UI.'
|
||||
)
|
||||
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
|
||||
else:
|
||||
App = None
|
||||
|
@@ -483,6 +483,11 @@ class JupyterHubSingleUser(ExtensionApp):
|
||||
cfg.answer_yes = True
|
||||
self.config.FileContentsManager.delete_to_trash = False
|
||||
|
||||
# load Spawner.notebook_dir configuration, if given
|
||||
root_dir = os.getenv("JUPYTERHUB_ROOT_DIR", None)
|
||||
if root_dir:
|
||||
cfg.root_dir = os.path.expanduser(root_dir)
|
||||
|
||||
# load http server config from environment
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
if url.port:
|
||||
@@ -511,16 +516,25 @@ class JupyterHubSingleUser(ExtensionApp):
|
||||
|
||||
# Jupyter Server default: config files have higher priority than extensions,
|
||||
# by:
|
||||
# 1. load config files
|
||||
# 1. load config files and CLI
|
||||
# 2. load extension config
|
||||
# 3. merge file config into extension config
|
||||
|
||||
# we invert that by merging our extension config into server config before
|
||||
# they get merged the other way
|
||||
# this way config from this extension should always have highest priority
|
||||
|
||||
# but this also puts our config above _CLI_ options,
|
||||
# and CLI should come before env,
|
||||
# so merge that into _our_ config before loading
|
||||
if self.serverapp.cli_config:
|
||||
for cls_name, cls_config in self.serverapp.cli_config.items():
|
||||
if cls_name in self.config:
|
||||
self.config[cls_name].merge(cls_config)
|
||||
|
||||
self.serverapp.update_config(self.config)
|
||||
|
||||
# add our custom templates
|
||||
# config below here has _lower_ priority than user config
|
||||
self.config.NotebookApp.extra_template_paths.append(SINGLEUSER_TEMPLATES_DIR)
|
||||
|
||||
@default("default_url")
|
||||
@@ -608,7 +622,9 @@ class JupyterHubSingleUser(ExtensionApp):
|
||||
app.web_app.settings[
|
||||
"page_config_hook"
|
||||
] = app.identity_provider.page_config_hook
|
||||
app.web_app.settings["log_function"] = log_request
|
||||
# if the user has configured a log function in the tornado settings, do not override it
|
||||
if not 'log_function' in app.config.ServerApp.get('tornado_settings', {}):
|
||||
app.web_app.settings["log_function"] = log_request
|
||||
# add jupyterhub version header
|
||||
headers = app.web_app.settings.setdefault("headers", {})
|
||||
headers["X-JupyterHub-Version"] = __version__
|
||||
|
@@ -669,7 +669,8 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
# load the hub-related settings into the tornado settings dict
|
||||
self.init_hub_auth()
|
||||
s = self.tornado_settings
|
||||
s['log_function'] = log_request
|
||||
# if the user has configured a log function in the tornado settings, do not override it
|
||||
s.setdefault('log_function', log_request)
|
||||
s['user'] = self.user
|
||||
s['group'] = self.group
|
||||
s['hub_prefix'] = self.hub_prefix
|
||||
|
@@ -382,6 +382,23 @@ class Spawner(LoggingConfigurable):
|
||||
scopes.append(f"access:servers!server={self.user.name}/{self.name}")
|
||||
return sorted(set(scopes))
|
||||
|
||||
server_token_scopes = Union(
|
||||
[List(Unicode()), Callable()],
|
||||
help="""The list of scopes to request for $JUPYTERHUB_API_TOKEN
|
||||
|
||||
If not specified, the scopes in the `server` role will be used
|
||||
(unchanged from pre-4.0).
|
||||
|
||||
If callable, will be called with the Spawner instance as its sole argument
|
||||
(JupyterHub user available as spawner.user).
|
||||
|
||||
JUPYTERHUB_API_TOKEN will be assigned the _subset_ of these scopes
|
||||
that are held by the user (as in oauth_client_allowed_scopes).
|
||||
|
||||
.. versionadded:: 4.0
|
||||
""",
|
||||
).tag(config=True)
|
||||
|
||||
will_resume = Bool(
|
||||
False,
|
||||
help="""Whether the Spawner will resume on next start
|
||||
|
14
jupyterhub/tests/browser/conftest.py
Normal file
14
jupyterhub/tests/browser/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def browser():
|
||||
# browser_type in ["chromium", "firefox", "webkit"]
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.firefox.launch(headless=True)
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
yield page
|
||||
await context.clear_cookies()
|
||||
await browser.close()
|
1070
jupyterhub/tests/browser/test_browser.py
Normal file
1070
jupyterhub/tests/browser/test_browser.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ class JupyterHubTestHandler(JupyterHandler):
|
||||
info = {
|
||||
"current_user": self.current_user,
|
||||
"config": self.app.config,
|
||||
"root_dir": self.contents_manager.root_dir,
|
||||
"disable_user_config": getattr(self.app, "disable_user_config", None),
|
||||
"settings": self.settings,
|
||||
"config_file_paths": self.app.config_file_paths,
|
||||
|
@@ -1,23 +0,0 @@
|
||||
import pytest
|
||||
from selenium import webdriver
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_session():
|
||||
"""Re-use one browser instance for the test session"""
|
||||
options = webdriver.FirefoxOptions()
|
||||
options.add_argument("-headless")
|
||||
driver = webdriver.Firefox(options=options)
|
||||
yield driver
|
||||
driver.close()
|
||||
driver.quit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def browser(browser_session, cleanup_after):
|
||||
"""Get the browser session for one test
|
||||
|
||||
cookies are cleared after each test
|
||||
"""
|
||||
yield browser_session
|
||||
browser_session.delete_all_cookies()
|
@@ -1,100 +0,0 @@
|
||||
"""Using for testing via the Selenium WebDriver for elements localization"""
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
class BarLocators:
|
||||
"""class for handling the Menu bar page locators"""
|
||||
|
||||
LINK_HOME_BAR = (By.CSS_SELECTOR, "div.container-fluid a")
|
||||
USER_NAME = (By.CLASS_NAME, 'navbar-text')
|
||||
|
||||
|
||||
class LoginPageLocators:
|
||||
"""class for handling the login page locators"""
|
||||
|
||||
FORM_LOGIN = (By.XPATH, '//*[@id="login-main"]/form')
|
||||
ACCOUNT = (By.ID, "username_input")
|
||||
PASSWORD = (By.ID, "password_input")
|
||||
ERROR_INVALID_CREDANTIALS = (By.CSS_SELECTOR, "p.login_error")
|
||||
|
||||
|
||||
class SpawningPageLocators:
|
||||
"""class for handling the Spawning page locators"""
|
||||
|
||||
BUTTONS_SERVER = (By.CSS_SELECTOR, "div.text-center a")
|
||||
TEXT_SERVER_TITLE = (By.CSS_SELECTOR, "div.text-center h1")
|
||||
|
||||
TEXT_SERVER = (By.CSS_SELECTOR, "div.text-center p")
|
||||
TEXT_SERVER_NOT_RUN_YET = "Server not running"
|
||||
TEXT_SERVER_NOT_RUNNING = "Your server is not running. Would you like to start it?"
|
||||
|
||||
TEXT_SERVER_STARTING = "Your server is starting up."
|
||||
TEXT_SERVER_REDIRECT = (
|
||||
"You will be redirected automatically when it's ready for you."
|
||||
)
|
||||
PROGRESS_MESSAGE = (By.ID, "progress-message")
|
||||
PROGRESS_PRO = (By.ID, "sr-progress")
|
||||
PROGRESS_STATUS = (By.CLASS_NAME, "sr-only")
|
||||
TEXT = (By.ID, "starting")
|
||||
|
||||
|
||||
class HomePageLocators:
|
||||
"""class for handling the home page locators"""
|
||||
|
||||
BUTTONS_SERVER = (By.CSS_SELECTOR, "div.text-center a")
|
||||
TEXT_SERVER = (By.CSS_SELECTOR, "div.text-center p")
|
||||
TEXT_SERVER_STARTING = "Your server is starting up."
|
||||
TEXT_SERVER_REDIRECT = (
|
||||
"You will be redirected automatically when it's ready for you."
|
||||
)
|
||||
|
||||
|
||||
class TokenPageLocators:
|
||||
"""class for handling the Token page locators"""
|
||||
|
||||
BUTTON_API_REQ = (
|
||||
By.XPATH,
|
||||
'//form[@id="request-token-form"]//button[@type="submit"]',
|
||||
)
|
||||
LIST_EXP_TOKEN_FIELD = (By.ID, "token-expiration-seconds")
|
||||
LIST_EXP_TOKEN_OPT = (By.XPATH, '//option')
|
||||
""" 1 Hour,1 Day,1 Week, Never """
|
||||
|
||||
LIST_EXP_TOKEN_OPT_DICT = {
|
||||
'1 Hour': '3600',
|
||||
'1 Day': '86400',
|
||||
'1 Week': '604800',
|
||||
'Never': '',
|
||||
}
|
||||
"""'1 Hour': '3600','1 Day': '86400','1 Week': '604800','Never': ''
|
||||
displayed options: the values in sec"""
|
||||
|
||||
NEVER_EXP = (By.XPATH, '//*[@id="token-expiration-seconds"]/option[4]')
|
||||
TEXT = "Copy this token. You won't be able to see it again, but you can always come back here to get a new one."
|
||||
|
||||
# API Tokens table
|
||||
TOKEN_TABLE = (By.XPATH, '//h2[text()="API Tokens"]//following::table')
|
||||
TOKEN_TABLE_HEADER = (By.XPATH, '//h2[text()="API Tokens"]//following::table/thead')
|
||||
TOKEN_TABLE_HEAD_LIST = ['Note', 'Last used', 'Created', 'Expires at']
|
||||
TOKEN_TABLE_BODY = (By.TAG_NAME, 'tbody')
|
||||
TOKEN_TABLE_ROWS_BY_CLASS = (
|
||||
By.XPATH,
|
||||
'//h2[text()="API Tokens"]//following::table//tr[@class="token-row"]',
|
||||
)
|
||||
BUTTON_REVOKE_TOKEN = (By.XPATH, '//tr/td[5]/button')
|
||||
|
||||
# Authorized Applications
|
||||
AUTH_TABLE = (By.XPATH, '//h2[text()="Authorized Applications"]//following::table')
|
||||
AUTH_TABLE_HEAD_LIST = ['Application', 'Last used', 'First authorized']
|
||||
AUTH_TABLE_HEADER = (
|
||||
By.XPATH,
|
||||
'//h2[text()="Authorized Applications"]//following::table/thead',
|
||||
)
|
||||
AUTH_TABLE_HEAD = (By.TAG_NAME, 'thead')
|
||||
AUTH_TABLE_BODY = (By.TAG_NAME, 'tbody')
|
||||
AUTH_TABLE_ROWS_BY_CLASS = (
|
||||
By.XPATH,
|
||||
'//h2[text()="Authorized Applications"]//following::table//tr[@class="token-row"]',
|
||||
)
|
||||
BUTTON_REVOKE_AUTH = (By.XPATH, '//tr/td[4]/button')
|
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,41 @@ async def test_xsrf_check(app, username, method, path, xsrf_in_url):
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
@mark.parametrize(
|
||||
"auth, expected_message",
|
||||
[
|
||||
("", "Missing or invalid credentials"),
|
||||
("cookie_no_xsrf", "'_xsrf' argument missing from GET"),
|
||||
("cookie_xsrf_mismatch", "XSRF cookie does not match GET argument"),
|
||||
("token_no_scope", "requires any of [list:users]"),
|
||||
("cookie_no_scope", "requires any of [list:users]"),
|
||||
],
|
||||
)
|
||||
async def test_permission_error_messages(app, user, auth, expected_message):
|
||||
# 1. no credentials, should be 403 and not mention xsrf
|
||||
|
||||
url = public_url(app, path="hub/api/users")
|
||||
|
||||
kwargs = {}
|
||||
kwargs["headers"] = headers = {}
|
||||
kwargs["params"] = params = {}
|
||||
if auth == "token_no_scope":
|
||||
token = user.new_api_token()
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif "cookie" in auth:
|
||||
cookies = kwargs["cookies"] = await app.login_user(user.name)
|
||||
if auth == "cookie_no_scope":
|
||||
params["_xsrf"] = cookies["_xsrf"]
|
||||
if auth == "cookie_xsrf_mismatch":
|
||||
params["_xsrf"] = "somethingelse"
|
||||
|
||||
r = await async_requests.get(url, **kwargs)
|
||||
assert r.status_code == 403
|
||||
response = r.json()
|
||||
message = response["message"]
|
||||
assert expected_message in message
|
||||
|
||||
|
||||
# --------------
|
||||
# User API tests
|
||||
# --------------
|
||||
|
@@ -35,7 +35,7 @@ def generate_old_db(env_dir, hub_version, db_url):
|
||||
pkgs.append('sqlalchemy<2')
|
||||
|
||||
if 'mysql' in db_url:
|
||||
pkgs.append('mysql-connector-python')
|
||||
pkgs.append('mysqlclient')
|
||||
elif 'postgres' in db_url:
|
||||
pkgs.append('psycopg2-binary')
|
||||
check_call([env_pip, 'install'] + pkgs)
|
||||
|
@@ -848,8 +848,12 @@ async def test_server_token_role(app):
|
||||
orm_server_token = orm.APIToken.find(app.db, server_token)
|
||||
assert orm_server_token
|
||||
|
||||
server_role = orm.Role.find(app.db, 'server')
|
||||
assert set(server_role.scopes) == set(orm_server_token.scopes)
|
||||
# resolve `!server` filter in server role
|
||||
server_role_scopes = {
|
||||
s.replace("!server", f"!server={user.name}/")
|
||||
for s in orm.Role.find(app.db, "server").scopes
|
||||
}
|
||||
assert set(orm_server_token.scopes) == server_role_scopes
|
||||
|
||||
assert orm_server_token.user.name == user.name
|
||||
assert user.api_tokens == [orm_server_token]
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import os
|
||||
import sys
|
||||
from contextlib import nullcontext
|
||||
from pprint import pprint
|
||||
from subprocess import CalledProcessError, check_output
|
||||
from unittest import mock
|
||||
from urllib.parse import urlencode, urlparse
|
||||
@@ -171,9 +172,7 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
||||
)
|
||||
r.raise_for_status()
|
||||
info = r.json()
|
||||
import pprint
|
||||
|
||||
pprint.pprint(info)
|
||||
pprint(info)
|
||||
assert info['disable_user_config']
|
||||
server_config = info['config']
|
||||
settings = info['settings']
|
||||
@@ -198,6 +197,79 @@ async def test_disable_user_config(request, app, tmpdir, full_spawn):
|
||||
assert_not_in_home(path, key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extension", [True, False])
|
||||
@pytest.mark.parametrize("notebook_dir", ["", "~", "~/sub", "ABS"])
|
||||
async def test_notebook_dir(
|
||||
request, app, tmpdir, user, full_spawn, extension, notebook_dir
|
||||
):
|
||||
if extension:
|
||||
try:
|
||||
import jupyter_server # noqa
|
||||
except ImportError:
|
||||
pytest.skip("needs jupyter-server 2")
|
||||
else:
|
||||
if jupyter_server.version_info < (2,):
|
||||
pytest.skip("needs jupyter-server 2")
|
||||
|
||||
token = user.new_api_token(scopes=["access:servers!user"])
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
spawner = user.spawner
|
||||
if extension:
|
||||
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "1"
|
||||
else:
|
||||
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "0"
|
||||
|
||||
home_dir = tmpdir.join("home").mkdir()
|
||||
sub_dir = home_dir.join("sub").mkdir()
|
||||
with sub_dir.join("subfile.txt").open("w") as f:
|
||||
f.write("txt\n")
|
||||
abs_dir = tmpdir.join("abs").mkdir()
|
||||
with abs_dir.join("absfile.txt").open("w") as f:
|
||||
f.write("absfile\n")
|
||||
|
||||
if notebook_dir:
|
||||
expected_root_dir = notebook_dir.replace("ABS", str(abs_dir)).replace(
|
||||
"~", str(home_dir)
|
||||
)
|
||||
else:
|
||||
expected_root_dir = str(home_dir)
|
||||
|
||||
spawner.notebook_dir = notebook_dir.replace("ABS", str(abs_dir))
|
||||
|
||||
# home_dir is defined on SimpleSpawner
|
||||
user.spawner.home_dir = home = str(home_dir)
|
||||
spawner.environment["HOME"] = home
|
||||
await user.spawn()
|
||||
await app.proxy.add_user(user)
|
||||
url = public_url(app, user)
|
||||
r = await async_requests.get(
|
||||
url_path_join(public_url(app, user), 'jupyterhub-test-info'), headers=headers
|
||||
)
|
||||
r.raise_for_status()
|
||||
info = r.json()
|
||||
pprint(info)
|
||||
|
||||
assert info["root_dir"] == expected_root_dir
|
||||
# secondary check: make sure it has the intended effect on root_dir
|
||||
r = await async_requests.get(
|
||||
url_path_join(public_url(app, user), 'api/contents/'), headers=headers
|
||||
)
|
||||
r.raise_for_status()
|
||||
root_contents = sorted(item['name'] for item in r.json()['content'])
|
||||
|
||||
# check contents
|
||||
if not notebook_dir or notebook_dir == "~":
|
||||
# use any to avoid counting possible automatically created files in $HOME
|
||||
assert 'sub' in root_contents
|
||||
elif notebook_dir == "ABS":
|
||||
assert 'absfile.txt' in root_contents
|
||||
elif notebook_dir == "~/sub":
|
||||
assert 'subfile.txt' in root_contents
|
||||
else:
|
||||
raise ValueError(f"No contents check for {notebook_dir}")
|
||||
|
||||
|
||||
def test_help_output():
|
||||
out = check_output(
|
||||
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
|
||||
|
@@ -20,7 +20,7 @@ from ..objects import Hub, Server
|
||||
from ..scopes import access_scopes
|
||||
from ..spawner import LocalProcessSpawner, Spawner
|
||||
from ..user import User
|
||||
from ..utils import AnyTimeoutError, new_token, url_path_join
|
||||
from ..utils import AnyTimeoutError, maybe_future, new_token, url_path_join
|
||||
from .mocking import public_url
|
||||
from .test_api import add_user
|
||||
from .utils import async_requests
|
||||
@@ -336,6 +336,12 @@ async def test_spawner_insert_api_token(app):
|
||||
assert found
|
||||
assert found.user.name == user.name
|
||||
assert user.api_tokens == [found]
|
||||
# resolve `!server` filter in server role
|
||||
server_role_scopes = {
|
||||
s.replace("!server", f"!server={user.name}/")
|
||||
for s in orm.Role.find(app.db, "server").scopes
|
||||
}
|
||||
assert set(found.scopes) == server_role_scopes
|
||||
await user.stop()
|
||||
|
||||
|
||||
@@ -361,6 +367,58 @@ async def test_spawner_bad_api_token(app):
|
||||
assert other_user.api_tokens == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"have_scopes, request_scopes, expected_scopes",
|
||||
[
|
||||
(["self"], ["inherit"], ["inherit"]),
|
||||
(["self"], [], ["access:servers!server=USER/", "users:activity!user"]),
|
||||
(
|
||||
["self"],
|
||||
["admin:groups", "read:servers!server"],
|
||||
["users:activity!user", "read:servers!server=USER/"],
|
||||
),
|
||||
(
|
||||
["self", "read:groups!group=x", "users:activity"],
|
||||
["admin:groups", "users:activity"],
|
||||
["read:groups!group=x", "read:groups:name!group=x", "users:activity"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_server_token_scopes(
|
||||
app, username, create_user_with_scopes, have_scopes, request_scopes, expected_scopes
|
||||
):
|
||||
"""Token provided by spawner is not in the db
|
||||
|
||||
Insert token into db as a user-provided token.
|
||||
"""
|
||||
db = app.db
|
||||
|
||||
# apply templating
|
||||
def _format_scopes(scopes):
|
||||
if callable(scopes):
|
||||
|
||||
async def get_scopes(*args):
|
||||
return _format_scopes(await maybe_future(scopes(*args)))
|
||||
|
||||
return get_scopes
|
||||
|
||||
return [s.replace("USER", username) for s in scopes]
|
||||
|
||||
have_scopes = _format_scopes(have_scopes)
|
||||
request_scopes = _format_scopes(request_scopes)
|
||||
expected_scopes = _format_scopes(expected_scopes)
|
||||
|
||||
user = create_user_with_scopes(*have_scopes, name=username)
|
||||
spawner = user.spawner
|
||||
spawner.server_token_scopes = request_scopes
|
||||
|
||||
await user.spawn()
|
||||
orm_token = orm.APIToken.find(db, spawner.api_token)
|
||||
assert orm_token
|
||||
assert set(orm_token.scopes) == set(expected_scopes)
|
||||
await user.stop()
|
||||
|
||||
|
||||
async def test_spawner_delete_server(app):
|
||||
"""Test deleting spawner.server
|
||||
|
||||
|
@@ -53,3 +53,16 @@ def test_sync_groups(app, user, group_names):
|
||||
assert user.orm_user in group.users
|
||||
else:
|
||||
assert user.orm_user not in group.users
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"server_name, path",
|
||||
[
|
||||
("", ""),
|
||||
("name", "name/"),
|
||||
("næme", "n%C3%A6me/"),
|
||||
],
|
||||
)
|
||||
def test_server_url(app, user, server_name, path):
|
||||
user_url = user.url
|
||||
assert user.server_url(server_name) == user_url + path
|
||||
|
@@ -13,7 +13,7 @@ from tornado import gen, web
|
||||
from tornado.httputil import urlencode
|
||||
from tornado.log import app_log
|
||||
|
||||
from . import orm
|
||||
from . import orm, roles, scopes
|
||||
from ._version import __version__, _check_version
|
||||
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
|
||||
from .metrics import RUNNING_SERVERS, TOTAL_USERS
|
||||
@@ -588,7 +588,7 @@ class User:
|
||||
if not server_name:
|
||||
return self.url
|
||||
else:
|
||||
return url_path_join(self.url, url_escape_path(server_name))
|
||||
return url_path_join(self.url, url_escape_path(server_name), "/")
|
||||
|
||||
def progress_url(self, server_name=''):
|
||||
"""API URL for progress endpoint for a server with a given name"""
|
||||
@@ -673,13 +673,63 @@ class User:
|
||||
orm_server = orm.Server(base_url=base_url)
|
||||
db.add(orm_server)
|
||||
note = "Server at %s" % base_url
|
||||
api_token = self.new_api_token(note=note, roles=['server'])
|
||||
db.commit()
|
||||
|
||||
spawner = self.get_spawner(server_name, replace_failed=True)
|
||||
spawner.server = server = Server(orm_server=orm_server)
|
||||
assert spawner.orm_spawner.server is orm_server
|
||||
|
||||
requested_scopes = spawner.server_token_scopes
|
||||
if callable(requested_scopes):
|
||||
requested_scopes = await maybe_future(requested_scopes(spawner))
|
||||
if not requested_scopes:
|
||||
# nothing requested, default to 'server' role
|
||||
requested_scopes = orm.Role.find(db, "server").scopes
|
||||
requested_scopes = set(requested_scopes)
|
||||
# resolve !server filter, which won't resolve elsewhere,
|
||||
# because this token is not owned by the server's own oauth client
|
||||
server_filter = f"={self.name}/{server_name}"
|
||||
requested_scopes = {
|
||||
scope + server_filter if scope.endswith("!server") else scope
|
||||
for scope in requested_scopes
|
||||
}
|
||||
# ensure activity scope is requested, since activity doesn't work without
|
||||
activity_scope = "users:activity!user"
|
||||
if not {activity_scope, "users:activity", "inherit"}.intersection(
|
||||
requested_scopes
|
||||
):
|
||||
self.log.warning(
|
||||
f"Adding required scope {activity_scope} to server token, missing from Spawner.server_token_scopes. Please make sure to add it!"
|
||||
)
|
||||
requested_scopes |= {activity_scope}
|
||||
|
||||
have_scopes = roles.roles_to_scopes(roles.get_roles_for(self.orm_user))
|
||||
have_scopes |= {"inherit"}
|
||||
jupyterhub_client = (
|
||||
db.query(orm.OAuthClient)
|
||||
.filter_by(
|
||||
identifier="jupyterhub",
|
||||
)
|
||||
.one()
|
||||
)
|
||||
|
||||
resolved_scopes, excluded_scopes = scopes._resolve_requested_scopes(
|
||||
requested_scopes, have_scopes, self.orm_user, jupyterhub_client, db
|
||||
)
|
||||
if excluded_scopes:
|
||||
# what level should this be?
|
||||
# for admins-get-more use case, this is going to happen for most users
|
||||
# but for misconfiguration, folks will want to know!
|
||||
self.log.debug(
|
||||
"Not assigning requested scopes for %s: requested=%s, assigned=%s, excluded=%s",
|
||||
spawner._log_name,
|
||||
requested_scopes,
|
||||
resolved_scopes,
|
||||
excluded_scopes,
|
||||
)
|
||||
|
||||
api_token = self.new_api_token(note=note, scopes=resolved_scopes)
|
||||
|
||||
# pass requesting handler to the spawner
|
||||
# e.g. for processing GET params
|
||||
spawner.handler = handler
|
||||
@@ -808,6 +858,7 @@ class User:
|
||||
spawner.api_token,
|
||||
generated=False,
|
||||
note="retrieved from spawner %s" % server_name,
|
||||
scopes=resolved_scopes,
|
||||
)
|
||||
# update OAuth client secret with updated API token
|
||||
if oauth_provider:
|
||||
|
@@ -229,9 +229,10 @@ async def exponential_backoff(
|
||||
# add some random jitter to improve performance
|
||||
# this prevents overloading any single tornado loop iteration with
|
||||
# too many things
|
||||
dt = min(max_wait, remaining, random.uniform(0, start_wait * scale))
|
||||
if dt < max_wait:
|
||||
limit = min(max_wait, start_wait * scale)
|
||||
if limit < max_wait:
|
||||
scale *= scale_factor
|
||||
dt = min(remaining, random.uniform(0, limit))
|
||||
await asyncio.sleep(dt)
|
||||
raise asyncio.TimeoutError(fail_message)
|
||||
|
||||
|
@@ -43,7 +43,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "4.0.0b1"
|
||||
current = "4.0.2"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
@@ -7,7 +7,7 @@
|
||||
asyncio_mode = auto
|
||||
|
||||
# jupyter_server plugin is incompatible with notebook imports
|
||||
addopts = -p no:jupyter_server -m 'not selenium' --color yes --durations 10 --verbose
|
||||
addopts = -p no:jupyter_server -m 'not browser' --color yes --durations 10 --verbose
|
||||
|
||||
python_files = test_*.py
|
||||
markers =
|
||||
@@ -17,7 +17,7 @@ markers =
|
||||
user: mark as a test for a user
|
||||
slow: mark a test as slow
|
||||
role: mark as a test for roles
|
||||
selenium: web tests that run with selenium
|
||||
browser: web tests that run with playwright
|
||||
|
||||
filterwarnings =
|
||||
ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning
|
||||
|
4
setup.py
4
setup.py
@@ -55,7 +55,7 @@ def get_package_data():
|
||||
'alembic/*',
|
||||
'alembic/versions/*',
|
||||
'event-schemas/*/*.yaml',
|
||||
'jupyterhub/singleuser/templates/*.html',
|
||||
'singleuser/templates/*.html',
|
||||
]
|
||||
return package_data
|
||||
|
||||
@@ -144,7 +144,7 @@ setup_args = dict(
|
||||
"pytest-asyncio>=0.17",
|
||||
"pytest-cov",
|
||||
"requests-mock",
|
||||
"selenium",
|
||||
"playwright",
|
||||
"virtualenv",
|
||||
],
|
||||
},
|
||||
|
@@ -1,299 +0,0 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
require(["jquery", "moment", "jhapi", "utils"], function (
|
||||
$,
|
||||
moment,
|
||||
JHAPI,
|
||||
utils,
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var base_url = window.jhdata.base_url;
|
||||
var prefix = window.jhdata.prefix;
|
||||
var admin_access = window.jhdata.admin_access;
|
||||
var options_form = window.jhdata.options_form;
|
||||
|
||||
var api = new JHAPI(base_url);
|
||||
|
||||
function getRow(element) {
|
||||
var original = element;
|
||||
while (!element.hasClass("server-row")) {
|
||||
element = element.parent();
|
||||
if (element[0].tagName === "BODY") {
|
||||
console.error("Couldn't find row for", original);
|
||||
throw new Error("No server-row found");
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function resort(col, order) {
|
||||
var query = window.location.search.slice(1).split("&");
|
||||
// if col already present in args, remove it
|
||||
var i = 0;
|
||||
while (i < query.length) {
|
||||
if (query[i] === "sort=" + col) {
|
||||
query.splice(i, 1);
|
||||
if (query[i] && query[i].substr(0, 6) === "order=") {
|
||||
query.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
// add new order to the front
|
||||
if (order) {
|
||||
query.unshift("order=" + order);
|
||||
}
|
||||
query.unshift("sort=" + col);
|
||||
// reload page with new order
|
||||
window.location = window.location.pathname + "?" + query.join("&");
|
||||
}
|
||||
|
||||
$("th").map(function (i, th) {
|
||||
th = $(th);
|
||||
var col = th.data("sort");
|
||||
if (!col || col.length === 0) {
|
||||
return;
|
||||
}
|
||||
var order = th.find("i").hasClass("fa-sort-desc") ? "asc" : "desc";
|
||||
th.find("a").click(function () {
|
||||
resort(col, order);
|
||||
});
|
||||
});
|
||||
|
||||
$(".time-col").map(function (i, el) {
|
||||
// convert ISO datestamps to nice momentjs ones
|
||||
el = $(el);
|
||||
var m = moment(new Date(el.text().trim()));
|
||||
el.text(m.isValid() ? m.fromNow() : "Never");
|
||||
});
|
||||
|
||||
$(".stop-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var serverName = row.data("server-name");
|
||||
var user = row.data("user");
|
||||
el.text("stopping...");
|
||||
var stop = function (options) {
|
||||
return api.stop_server(user, options);
|
||||
};
|
||||
if (serverName !== "") {
|
||||
stop = function (options) {
|
||||
return api.stop_named_server(user, serverName, options);
|
||||
};
|
||||
}
|
||||
stop({
|
||||
success: function () {
|
||||
el.text("stop " + serverName).addClass("hidden");
|
||||
row.find(".access-server").addClass("hidden");
|
||||
row.find(".start-server").removeClass("hidden");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(".delete-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var serverName = row.data("server-name");
|
||||
var user = row.data("user");
|
||||
el.text("deleting...");
|
||||
api.delete_named_server(user, serverName, {
|
||||
success: function () {
|
||||
row.remove();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(".access-server").map(function (i, el) {
|
||||
el = $(el);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.attr(
|
||||
"href",
|
||||
utils.url_path_join(prefix, "user", user, serverName) + "/",
|
||||
);
|
||||
});
|
||||
|
||||
if (admin_access && options_form) {
|
||||
// if admin access and options form are enabled
|
||||
// link to spawn page instead of making API requests
|
||||
$(".start-server").map(function (i, el) {
|
||||
el = $(el);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.attr(
|
||||
"href",
|
||||
utils.url_path_join(prefix, "hub/spawn", user, serverName),
|
||||
);
|
||||
});
|
||||
// cannot start all servers in this case
|
||||
// since it would mean opening a bunch of tabs
|
||||
$("#start-all-servers").addClass("hidden");
|
||||
} else {
|
||||
$(".start-server").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.text("starting...");
|
||||
var start = function (options) {
|
||||
return api.start_server(user, options);
|
||||
};
|
||||
if (serverName !== "") {
|
||||
start = function (options) {
|
||||
return api.start_named_server(user, serverName, options);
|
||||
};
|
||||
}
|
||||
start({
|
||||
success: function () {
|
||||
el.text("start " + serverName).addClass("hidden");
|
||||
row.find(".stop-server").removeClass("hidden");
|
||||
row.find(".access-server").removeClass("hidden");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(".edit-user").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var admin = row.data("admin");
|
||||
var dialog = $("#edit-user-dialog");
|
||||
dialog.data("user", user);
|
||||
dialog.find(".username-input").val(user);
|
||||
dialog.find(".admin-checkbox").attr("checked", admin === "True");
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#edit-user-dialog")
|
||||
.find(".save-button")
|
||||
.click(function () {
|
||||
var dialog = $("#edit-user-dialog");
|
||||
var user = dialog.data("user");
|
||||
var name = dialog.find(".username-input").val();
|
||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||
api.edit_user(
|
||||
user,
|
||||
{
|
||||
admin: admin,
|
||||
name: name,
|
||||
},
|
||||
{
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$(".delete-user").click(function () {
|
||||
var el = $(this);
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var dialog = $("#delete-user-dialog");
|
||||
dialog.find(".delete-username").text(user);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#delete-user-dialog")
|
||||
.find(".delete-button")
|
||||
.click(function () {
|
||||
var dialog = $("#delete-user-dialog");
|
||||
var username = dialog.find(".delete-username").text();
|
||||
console.log("deleting", username);
|
||||
api.delete_user(username, {
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$("#add-users").click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
dialog.find(".username-input").val("");
|
||||
dialog.find(".admin-checkbox").prop("checked", false);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#add-users-dialog")
|
||||
.find(".save-button")
|
||||
.click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
var lines = dialog.find(".username-input").val().split("\n");
|
||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||
var usernames = [];
|
||||
lines.map(function (line) {
|
||||
var username = line.trim();
|
||||
if (username.length) {
|
||||
usernames.push(username);
|
||||
}
|
||||
});
|
||||
|
||||
api.add_users(
|
||||
usernames,
|
||||
{ admin: admin },
|
||||
{
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$("#stop-all-servers").click(function () {
|
||||
$("#stop-all-servers-dialog").modal();
|
||||
});
|
||||
|
||||
$("#start-all-servers").click(function () {
|
||||
$("#start-all-servers-dialog").modal();
|
||||
});
|
||||
|
||||
$("#stop-all-servers-dialog")
|
||||
.find(".stop-all-button")
|
||||
.click(function () {
|
||||
// stop all clicks all the active stop buttons
|
||||
$(".stop-server").not(".hidden").click();
|
||||
});
|
||||
|
||||
function start(el) {
|
||||
return function () {
|
||||
$(el).click();
|
||||
};
|
||||
}
|
||||
|
||||
$("#start-all-servers-dialog")
|
||||
.find(".start-all-button")
|
||||
.click(function () {
|
||||
$(".start-server")
|
||||
.not(".hidden")
|
||||
.each(function (i) {
|
||||
setTimeout(start(this), i * 500);
|
||||
});
|
||||
});
|
||||
|
||||
$("#shutdown-hub").click(function () {
|
||||
var dialog = $("#shutdown-hub-dialog");
|
||||
dialog.find("input[type=checkbox]").prop("checked", true);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#shutdown-hub-dialog")
|
||||
.find(".shutdown-button")
|
||||
.click(function () {
|
||||
var dialog = $("#shutdown-hub-dialog");
|
||||
var servers = dialog.find(".shutdown-servers-checkbox").prop("checked");
|
||||
var proxy = dialog.find(".shutdown-proxy-checkbox").prop("checked");
|
||||
api.shutdown_hub({
|
||||
proxy: proxy,
|
||||
servers: servers,
|
||||
});
|
||||
});
|
||||
|
||||
// signal that page has finished loading
|
||||
window._jupyterhub_page_loaded = true;
|
||||
});
|
@@ -66,7 +66,9 @@
|
||||
// common form display
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px @jupyter-orange;
|
||||
box-shadow:
|
||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px @jupyter-orange;
|
||||
border-color: @jupyter-orange;
|
||||
outline-color: @jupyter-orange;
|
||||
}
|
||||
|
@@ -17,10 +17,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
require(["admin"]);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
This note will help you keep track of what your tokens are for.
|
||||
</small>
|
||||
<br><br>
|
||||
<label for="token-expiration-seconds">Token expires</label>
|
||||
<label for="token-expiration-seconds">Token expires in</label>
|
||||
{% block expiration_options %}
|
||||
<select id="token-expiration-seconds"
|
||||
class="form-control">
|
||||
@@ -33,7 +33,7 @@
|
||||
</select>
|
||||
{% endblock expiration_options %}
|
||||
<small id="note-expires-at" class="form-text text-muted">
|
||||
You can configure when your token will be expired.
|
||||
You can configure when your token will expire.
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
@@ -62,8 +62,8 @@
|
||||
<div class="row">
|
||||
<h2>API Tokens</h2>
|
||||
<p>
|
||||
These are tokens with full access to the JupyterHub API.
|
||||
Anything you can do with JupyterHub can be done with these tokens.
|
||||
These are tokens with access to the JupyterHub API.
|
||||
Permissions for each token may be viewed via the JupyterHub tokens API.
|
||||
Revoking the API token for a running server will require restarting that server.
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
@@ -72,7 +72,7 @@
|
||||
<th>Note</th>
|
||||
<th>Last used</th>
|
||||
<th>Created</th>
|
||||
<th>Expires at</th>
|
||||
<th>Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Build as jupyterhub/singleuser
|
||||
# Run with the DockerSpawner in JupyterHub
|
||||
|
||||
ARG BASE_IMAGE=jupyter/base-notebook:2023-01-30
|
||||
ARG BASE_IMAGE=jupyter/base-notebook
|
||||
FROM $BASE_IMAGE
|
||||
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
||||
|
||||
|
Reference in New Issue
Block a user