diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c724be77 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# dependabot.yml 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 +# https://github.com/jupyterhub/jupyterhub/network/updates. +# +version: 2 +updates: + # Maintain dependencies in our GitHub Workflows + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "05:00" + timezone: "Etc/UTC" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 264aaea1..09d14c50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,12 +32,12 @@ jobs: build-release: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: "3.9" - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: "14" @@ -63,7 +63,7 @@ jobs: ./ci/check_installed_data.py # ref: https://github.com/actions/upload-artifact#readme - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: jupyterhub-${{ github.sha }} path: "dist/*" @@ -98,16 +98,16 @@ jobs: echo "REGISTRY=localhost:5000/" >> $GITHUB_ENV fi - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # Setup docker to build for multiple platforms, see: # https://github.com/docker/build-push-action/tree/v2.4.0#usage # https://github.com/docker/build-push-action/blob/v2.4.0/docs/advanced/multi-platform.md - name: Set up QEMU (for docker buildx) - uses: docker/setup-qemu-action@25f0500ff22e406f7191a2a8ba8cda16901ca018 # associated tag: v1.0.2 + uses: docker/setup-qemu-action@8b122486cedac8393e77aa9734c3528886e4a1a8 # associated tag: v1.0.2 - name: Set up Docker Buildx (for multi-arch builds) - uses: docker/setup-buildx-action@2a4b53665e15ce7d7049afb11ff1f70ff1610609 # associated tag: v1.1.2 + uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6 # associated tag: v1.1.2 with: # Allows pushing to registry on localhost:5000 driver-opts: network=host @@ -145,7 +145,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub - uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f + uses: docker/build-push-action@1cb9d22b932e4832bb29793b7777ec860fc1cde0 with: context: . platforms: linux/amd64,linux/arm64 @@ -166,7 +166,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub-onbuild - uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f + uses: docker/build-push-action@1cb9d22b932e4832bb29793b7777ec860fc1cde0 with: build-args: | BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }} @@ -187,7 +187,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub-demo - uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f + uses: docker/build-push-action@1cb9d22b932e4832bb29793b7777ec860fc1cde0 with: build-args: | BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }} @@ -211,7 +211,7 @@ jobs: branchRegex: ^\w[\w-.]*$ - name: Build and push jupyterhub/singleuser - uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f + uses: docker/build-push-action@1cb9d22b932e4832bb29793b7777ec860fc1cde0 with: build-args: | JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }} diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index abc33b6e..378ccab2 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -15,15 +15,13 @@ on: - "docs/**" - "jupyterhub/_version.py" - "jupyterhub/scopes.py" - - ".github/workflows/*" - - "!.github/workflows/test-docs.yml" + - ".github/workflows/test-docs.yml" push: paths: - "docs/**" - "jupyterhub/_version.py" - "jupyterhub/scopes.py" - - ".github/workflows/*" - - "!.github/workflows/test-docs.yml" + - ".github/workflows/test-docs.yml" branches-ignore: - "dependabot/**" - "pre-commit-ci-update-config" @@ -40,18 +38,18 @@ jobs: validate-rest-api-definition: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Validate REST API definition - uses: char0n/swagger-editor-validate@182d1a5d26ff5c2f4f452c43bd55e2c7d8064003 + uses: char0n/swagger-editor-validate@v1.3.1 with: definition-file: docs/source/_static/rest-api.yml test-docs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: "3.9" diff --git a/.github/workflows/test-jsx.yml b/.github/workflows/test-jsx.yml index fdc02a2d..02e8bd07 100644 --- a/.github/workflows/test-jsx.yml +++ b/.github/workflows/test-jsx.yml @@ -19,6 +19,9 @@ on: - "**" workflow_dispatch: +permissions: + contents: read + jobs: # The ./jsx folder contains React based source code files that are to compile # to share/jupyterhub/static/js/admin-react.js. The ./jsx folder includes @@ -29,8 +32,8 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: "14" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea6e3c32..ee9844e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,9 @@ env: LANG: C.UTF-8 PYTEST_ADDOPTS: "--verbose --color=yes" +permissions: + contents: read + jobs: # Run "pytest jupyterhub/tests" in various configurations pytest: @@ -53,9 +56,9 @@ jobs: # Tests everything when JupyterHub works against a dedicated mysql or # postgresql server. # - # nbclassic: + # legacy_notebook: # Tests everything when the user instances are started with - # notebook instead of jupyter_server. + # the legacy notebook server instead of jupyter_server. # # ssl: # Tests everything using internal SSL connections instead of @@ -69,20 +72,24 @@ jobs: # GitHub UI when the workflow run, we avoid using true/false as # values by instead duplicating the name to signal true. include: - - python: "3.6" + - python: "3.7" oldest_dependencies: oldest_dependencies - nbclassic: nbclassic - - python: "3.6" - subdomain: subdomain - - python: "3.7" - db: mysql - - python: "3.7" - ssl: ssl + legacy_notebook: legacy_notebook - python: "3.8" - db: postgres - - python: "3.8" - nbclassic: nbclassic + legacy_notebook: legacy_notebook - python: "3.9" + db: mysql + - python: "3.10" + db: postgres + - python: "3.10" + subdomain: subdomain + - python: "3.10" + ssl: ssl + # can't test 3.11.0-beta.4 until a greenlet release + # greenlet is a dependency of sqlalchemy on linux + # see https://github.com/gevent/gevent/issues/1867 + # - python: "3.11.0-beta.4" + - python: "3.10" main_dependencies: main_dependencies steps: @@ -110,11 +117,11 @@ jobs: if [ "${{ matrix.jupyter_server }}" != "" ]; then echo "JUPYTERHUB_SINGLEUSER_APP=jupyterhub.tests.mockserverapp.MockServerApp" >> $GITHUB_ENV fi - - uses: actions/checkout@v2 - # NOTE: actions/setup-node@v1 make use of a cache within the GitHub base + - uses: actions/checkout@v3 + # NOTE: actions/setup-node@v3 make use of a cache within the GitHub base # environment and setup in a fraction of a second. - name: Install Node v14 - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: "14" - name: Install Javascript dependencies @@ -123,12 +130,12 @@ jobs: npm install -g configurable-http-proxy yarn npm list - # NOTE: actions/setup-python@v2 make use of a cache within the GitHub base + # NOTE: actions/setup-python@v4 make use of a cache within the GitHub base # environment and setup in a fraction of a second. - name: Install Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: "${{ matrix.python }}" - name: Install Python dependencies run: | pip install --upgrade pip @@ -145,9 +152,9 @@ jobs: if [ "${{ matrix.main_dependencies }}" != "" ]; then pip install git+https://github.com/ipython/traitlets#egg=traitlets --force fi - if [ "${{ matrix.nbclassic }}" != "" ]; then + if [ "${{ matrix.legacy_notebook }}" != "" ]; then pip uninstall jupyter_server --yes - pip install notebook + pip install 'notebook<7' fi if [ "${{ matrix.db }}" == "mysql" ]; then pip install mysql-connector-python @@ -211,7 +218,7 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: build images run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01cd183b..fdec09a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # Autoformat: Python code, syntax patterns are modernized - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.37.3 hooks: - id: pyupgrade args: @@ -25,19 +25,19 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black # Autoformat: markdown, yaml, javascript (see the file .prettierignore) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.2 + rev: v2.7.1 hooks: - id: prettier # Autoformat and linting, misc. details - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: end-of-file-fixer exclude: share/jupyterhub/static/js/admin-react.js @@ -47,6 +47,6 @@ repos: # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "4.0.1" + rev: "5.0.2" hooks: - id: flake8 diff --git a/Dockerfile b/Dockerfile index ea8ab48f..13ec848b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ # your jupyterhub_config.py will be added automatically # from your docker directory. -ARG BASE_IMAGE=ubuntu:focal-20200729 +ARG BASE_IMAGE=ubuntu:22.04 FROM $BASE_IMAGE AS builder USER root diff --git a/dev-requirements.txt b/dev-requirements.txt index bca2e4b0..a7bdaefe 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,10 +9,13 @@ cryptography html5lib # needed for beautifulsoup jupyterlab >=3 mock +# nbclassic provides the '/tree/' handler, which we use in tests +# it is a transitive dependency via jupyterlab, +# but depend on it directly +nbclassic pre-commit pytest>=3.3 -pytest-asyncio; python_version < "3.7" -pytest-asyncio>=0.17; python_version >= "3.7" +pytest-asyncio>=0.17 pytest-cov requests-mock tbump diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index 99a1e93b..d02857dd 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -6,7 +6,7 @@ info: description: The REST API for JupyterHub license: name: BSD-3-Clause - version: 2.3.0.dev + version: 2.4.0.dev servers: - url: /hub/api security: diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 87007311..661b3832 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -183,12 +183,6 @@ itself, ``jupyterhub_config.py``, as a binary string: c.JupyterHub.cookie_secret = bytes.fromhex('64 CHAR HEX STRING') - -.. important:: - - If the cookie secret value changes for the Hub, all single-user notebook - servers must also be restarted. - .. _cookies: Cookies used by JupyterHub authentication diff --git a/examples/server-api/jupyterhub_config.py b/examples/server-api/jupyterhub_config.py index 110b6d10..8a0af9dd 100644 --- a/examples/server-api/jupyterhub_config.py +++ b/examples/server-api/jupyterhub_config.py @@ -2,6 +2,8 @@ # 1. start/stop servers, and # 2. access the server API +c = get_config() # noqa + c.JupyterHub.load_roles = [ { "name": "launcher", diff --git a/jsx/package.json b/jsx/package.json index cfd0edb1..8ac6f020 100644 --- a/jsx/package.json +++ b/jsx/package.json @@ -28,17 +28,7 @@ } }, "dependencies": { - "@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", - "@testing-library/user-event": "^13.5.0", - "babel-loader": "^8.2.1", "bootstrap": "^4.5.3", - "css-loader": "^5.0.1", - "eslint-plugin-unused-imports": "^1.1.1", - "file-loader": "^6.2.0", "history": "^5.0.0", "lodash.debounce": "^4.0.8", "prop-types": "^15.7.2", @@ -51,24 +41,35 @@ "react-redux": "^7.2.2", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "recompose": "^0.30.0", + "recompose": "npm:react-recompose@^0.31.2", "redux": "^4.0.5", - "regenerator-runtime": "^0.13.9", - "style-loader": "^2.0.0", - "webpack": "^5.6.0", - "webpack-cli": "^3.3.4", - "webpack-dev-server": "^3.11.0" + "regenerator-runtime": "^0.13.9" }, "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", + "@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", + "file-loader": "^6.2.0", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "prettier": "^2.2.1", - "sinon": "^13.0.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" } } diff --git a/jsx/src/components/AddUser/AddUser.jsx b/jsx/src/components/AddUser/AddUser.jsx index bcd1f639..3826ad2c 100644 --- a/jsx/src/components/AddUser/AddUser.jsx +++ b/jsx/src/components/AddUser/AddUser.jsx @@ -60,7 +60,10 @@ const AddUser = (props) => { placeholder="usernames separated by line" data-testid="user-textarea" onBlur={(e) => { - let split_users = e.target.value.split("\n"); + let split_users = e.target.value + .split("\n") + .map((u) => u.trim()) + .filter((u) => u.length > 0); setUsers(split_users); }} > @@ -88,17 +91,7 @@ const AddUser = (props) => { data-testid="submit" className="btn btn-primary" onClick={() => { - let filtered_users = users.filter( - (e) => - e.length > 2 && - /[!@#$%^&*(),.?":{}|<>]/g.test(e) == false - ); - if (filtered_users.length < users.length) { - setUsers(filtered_users); - failRegexEvent(); - } - - addUsers(filtered_users, admin) + addUsers(users, admin) .then((data) => data.status < 300 ? updateUsers(0, limit) diff --git a/jsx/src/components/AddUser/AddUser.test.js b/jsx/src/components/AddUser/AddUser.test.js index 0bc776f5..e3965f15 100644 --- a/jsx/src/components/AddUser/AddUser.test.js +++ b/jsx/src/components/AddUser/AddUser.test.js @@ -70,12 +70,12 @@ test("Removes users when they fail Regex", async () => { let textarea = screen.getByTestId("user-textarea"); let submit = screen.getByTestId("submit"); - fireEvent.blur(textarea, { target: { value: "foo\nbar\n!!*&*" } }); + fireEvent.blur(textarea, { target: { value: "foo \n bar\na@b.co\n \n\n" } }); await act(async () => { fireEvent.click(submit); }); - expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar"], false); + expect(callbackSpy).toHaveBeenCalledWith(["foo", "bar", "a@b.co"], false); }); test("Correctly submits admin", async () => { diff --git a/jsx/src/components/CreateGroup/CreateGroup.jsx b/jsx/src/components/CreateGroup/CreateGroup.jsx index b3302f25..66b8b9d8 100644 --- a/jsx/src/components/CreateGroup/CreateGroup.jsx +++ b/jsx/src/components/CreateGroup/CreateGroup.jsx @@ -59,7 +59,7 @@ const CreateGroup = (props) => { value={groupName} placeholder="group name..." onChange={(e) => { - setGroupName(e.target.value); + setGroupName(e.target.value.trim()); }} > diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.jsx b/jsx/src/components/ServerDashboard/ServerDashboard.jsx index 0fe40992..94c8e6d3 100644 --- a/jsx/src/components/ServerDashboard/ServerDashboard.jsx +++ b/jsx/src/components/ServerDashboard/ServerDashboard.jsx @@ -30,7 +30,7 @@ const AccessServerButton = ({ url }) => ( ); const ServerDashboard = (props) => { - let base_url = window.base_url; + let base_url = window.base_url || "/"; // sort methods var usernameDesc = (e) => e.sort((a, b) => (a.name > b.name ? 1 : -1)), usernameAsc = (e) => e.sort((a, b) => (a.name < b.name ? 1 : -1)), @@ -201,6 +201,25 @@ const ServerDashboard = (props) => { }; const ServerRowTable = ({ data }) => { + const sortedData = Object.keys(data) + .sort() + .reduce(function (result, key) { + let value = data[key]; + switch (key) { + case "last_activity": + case "created": + case "started": + // format timestamps + value = value ? timeSince(value) : value; + break; + } + if (Array.isArray(value)) { + // cast arrays (e.g. roles, groups) to string + value = value.sort().join(", "); + } + result[key] = value; + return result; + }, {}); return ( { valueStyle={{ padding: "4px", }} - data={data} + data={sortedData} /> ); }; @@ -251,11 +270,7 @@ const ServerDashboard = (props) => { {user.admin ? "admin" : ""} - {server.name ? ( -

{server.name}

- ) : ( -

[MAIN]

- )} +

{server.name}

{server.last_activity ? timeSince(server.last_activity) : "Never"} @@ -277,7 +292,7 @@ const ServerDashboard = (props) => { />