Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-jupyter-server for
openSUSE:Factory checked in at 2024-03-07 22:01:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jupyter-server (Old)
and /work/SRC/openSUSE:Factory/.python-jupyter-server.new.1770 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jupyter-server"
Thu Mar 7 22:01:21 2024 rev:40 rq:1155890 version:2.13.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-jupyter-server/python-jupyter-server.changes
2024-01-21 23:10:42.102635125 +0100
+++
/work/SRC/openSUSE:Factory/.python-jupyter-server.new.1770/python-jupyter-server.changes
2024-03-07 22:01:22.532469679 +0100
@@ -1,0 +2,15 @@
+Thu Mar 7 12:13:10 UTC 2024 - Ben Greiner <[email protected]>
+
+- Update to 2.13.0
+ ## Enhancements made
+ * Add an option to have authentication enabled for all endpoints
+ by default #1392 (@krassowski)
+ * websockets: add configurations for ping interval and timeout
+ #1391 (@oliver-sanders)
+ ## Bugs fixed
+ * Fix color in windows log console with colorama #1397
+ (@hansepac)
+- Skip building and using python39-jupyter-server-test: no longer
+ supported since ipython 8.19 (through ipykernel)
+
+-------------------------------------------------------------------
Old:
----
jupyter_server-2.12.5.tar.gz
New:
----
jupyter_server-2.13.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-jupyter-server.spec ++++++
--- /var/tmp/diff_new_pack.uXMTAC/_old 2024-03-07 22:01:23.396501454 +0100
+++ /var/tmp/diff_new_pack.uXMTAC/_new 2024-03-07 22:01:23.400501602 +0100
@@ -1,5 +1,5 @@
#
-# spec file
+# spec file for package python-jupyter-server
#
# Copyright (c) 2024 SUSE LLC
#
@@ -19,6 +19,7 @@
%global flavor @BUILD_FLAVOR@%{nil}
%if "%{flavor}" == "test"
%define psuffix -test
+%define skip_python39 1
%bcond_without test
%else
%define psuffix %{nil}
@@ -32,7 +33,7 @@
%endif
Name: python-jupyter-server%{psuffix}
-Version: 2.12.5
+Version: 2.13.0
Release: 0
Summary: The backend to Jupyter web applications
License: BSD-3-Clause
@@ -78,7 +79,7 @@
Requires: alts
%else
Requires(post): update-alternatives
-Requires(postun):update-alternatives
+Requires(postun): update-alternatives
%endif
%if "%{python_flavor}" == "python3" || "%{python_provides}" == "python3"
Provides: jupyter-jupyter-server = %{version}-%{release}
@@ -101,7 +102,7 @@
Requires: python-jupyter-server = %{version}
Requires: python-pytest >= 7
Requires: python-pytest-console-scripts
-Requires: python-pytest-jupyter-server >= 0.4
+Requires: python-pytest-jupyter-server >= 0.7
Requires: python-pytest-timeout
Requires: python-requests
@@ -154,7 +155,9 @@
%{python_sitelib}/jupyter_server
%{python_sitelib}/jupyter_server-%{version}*-info
+%if 0%{python_version_nodots} >= 310
%files %{python_files test}
%license LICENSE
%endif
+%endif
++++++ jupyter_server-2.12.5.tar.gz -> jupyter_server-2.13.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/.github/workflows/prep-release.yml
new/jupyter_server-2.13.0/.github/workflows/prep-release.yml
--- old/jupyter_server-2.12.5/.github/workflows/prep-release.yml
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/.github/workflows/prep-release.yml
2020-02-02 01:00:00.000000000 +0100
@@ -12,6 +12,10 @@
post_version_spec:
description: "Post Version Specifier"
required: false
+ silent:
+ description: "Set a placeholder in the changelog and don't publish the
release."
+ required: false
+ type: boolean
since:
description: "Use PRs with activity since this date or git reference"
required: false
@@ -22,6 +26,8 @@
jobs:
prep_release:
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -29,8 +35,9 @@
id: prep-release
uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
with:
- token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
+ token: ${{ secrets.GITHUB_TOKEN }}
version_spec: ${{ github.event.inputs.version_spec }}
+ silent: ${{ github.event.inputs.silent }}
post_version_spec: ${{ github.event.inputs.post_version_spec }}
target: ${{ github.event.inputs.target }}
branch: ${{ github.event.inputs.branch }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/.github/workflows/publish-changelog.yml
new/jupyter_server-2.13.0/.github/workflows/publish-changelog.yml
--- old/jupyter_server-2.12.5/.github/workflows/publish-changelog.yml
1970-01-01 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/.github/workflows/publish-changelog.yml
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,34 @@
+name: "Publish Changelog"
+on:
+ release:
+ types: [published]
+
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: "The branch to target"
+ required: false
+
+jobs:
+ publish_changelog:
+ runs-on: ubuntu-latest
+ environment: release
+ steps:
+ - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+
+ - uses: actions/create-github-app-token@v1
+ id: app-token
+ with:
+ app-id: ${{ vars.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
+ - name: Publish changelog
+ id: publish-changelog
+ uses:
jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2
+ with:
+ token: ${{ steps.app-token.outputs.token }}
+ branch: ${{ github.event.inputs.branch }}
+
+ - name: "** Next Step **"
+ run: |
+ echo "Merge the changelog update PR: ${{
steps.publish-changelog.outputs.pr_url }}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/.github/workflows/publish-release.yml
new/jupyter_server-2.13.0/.github/workflows/publish-release.yml
--- old/jupyter_server-2.12.5/.github/workflows/publish-release.yml
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/.github/workflows/publish-release.yml
2020-02-02 01:00:00.000000000 +0100
@@ -15,30 +15,32 @@
jobs:
publish_release:
runs-on: ubuntu-latest
+ environment: release
+ permissions:
+ id-token: write
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
+ - uses: actions/create-github-app-token@v1
+ id: app-token
+ with:
+ app-id: ${{ vars.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
- name: Populate Release
id: populate-release
uses:
jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
with:
- token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
- target: ${{ github.event.inputs.target }}
+ token: ${{ steps.app-token.outputs.token }}
branch: ${{ github.event.inputs.branch }}
release_url: ${{ github.event.inputs.release_url }}
steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
- name: Finalize Release
id: finalize-release
- env:
- PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }}
- TWINE_USERNAME: __token__
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses:
jupyter-server/jupyter-releaser/.github/actions/finalize-release@v2
+ uses:
jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
with:
- token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
- target: ${{ github.event.inputs.target }}
+ token: ${{ steps.app-token.outputs.token }}
release_url: ${{ steps.populate-release.outputs.release_url }}
- name: "** Next Step **"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/.pre-commit-config.yaml
new/jupyter_server-2.13.0/.pre-commit-config.yaml
--- old/jupyter_server-2.12.5/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
@@ -21,7 +21,7 @@
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.27.3
+ rev: 0.27.4
hooks:
- id: check-github-workflows
@@ -61,7 +61,7 @@
["traitlets>=5.13", "jupyter_core>=5.5", "jupyter_client>=8.5"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.9
+ rev: v0.2.0
hooks:
- id: ruff
types_or: [python, jupyter]
@@ -70,7 +70,7 @@
types_or: [python, jupyter]
- repo: https://github.com/scientific-python/cookie
- rev: "2023.12.21"
+ rev: "2024.01.24"
hooks:
- id: sp-repo-review
additional_dependencies: ["repo-review[cli]"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/CHANGELOG.md
new/jupyter_server-2.13.0/CHANGELOG.md
--- old/jupyter_server-2.12.5/CHANGELOG.md 2020-02-02 01:00:00.000000000
+0100
+++ new/jupyter_server-2.13.0/CHANGELOG.md 2020-02-02 01:00:00.000000000
+0100
@@ -4,6 +4,38 @@
<!-- <START NEW CHANGELOG ENTRY> -->
+## 2.13.0
+
+([Full
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.12.5...1369a5364d36a977fbec5957ed21d69acbbeda5a))
+
+### Enhancements made
+
+- Add an option to have authentication enabled for all endpoints by default
[#1392](https://github.com/jupyter-server/jupyter_server/pull/1392)
([@krassowski](https://github.com/krassowski))
+- websockets: add configurations for ping interval and timeout
[#1391](https://github.com/jupyter-server/jupyter_server/pull/1391)
([@oliver-sanders](https://github.com/oliver-sanders))
+
+### Bugs fixed
+
+- Fix color in windows log console with colorama
[#1397](https://github.com/jupyter-server/jupyter_server/pull/1397)
([@hansepac](https://github.com/hansepac))
+
+### Maintenance and upkeep improvements
+
+- Update release workflows
[#1399](https://github.com/jupyter-server/jupyter_server/pull/1399)
([@blink1073](https://github.com/blink1073))
+- chore: update pre-commit hooks
[#1390](https://github.com/jupyter-server/jupyter_server/pull/1390)
([@pre-commit-ci](https://github.com/pre-commit-ci))
+
+### Documentation improvements
+
+- Add deprecation note for `ServerApp.preferred_dir`
[#1396](https://github.com/jupyter-server/jupyter_server/pull/1396)
([@krassowski](https://github.com/krassowski))
+- Replace \_jupyter_server_extension_paths in apidocs
[#1393](https://github.com/jupyter-server/jupyter_server/pull/1393)
([@manics](https://github.com/manics))
+- fix "Shutdown" -> "Shut down"
[#1389](https://github.com/jupyter-server/jupyter_server/pull/1389)
([@Timeroot](https://github.com/Timeroot))
+
+### Contributors to this release
+
+([GitHub contributors page for this
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2024-01-16&to=2024-03-04&type=c))
+
+[@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ablink1073+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@hansepac](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ahansepac+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@manics](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Amanics+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@minrk](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@oliver-sanders](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aoliver-sanders+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Apre-commit-c
i+updated%3A2024-01-16..2024-03-04&type=Issues) |
[@Timeroot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ATimeroot+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@welcome](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Awelcome+updated%3A2024-01-16..2024-03-04&type=Issues)
|
[@yuvipanda](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ayuvipanda+updated%3A2024-01-16..2024-03-04&type=Issues)
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
## 2.12.5
([Full
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.12.4...a3a9d3deea7a798d13fe09a41e53f6f825caf21b))
@@ -18,8 +50,6 @@
[@blink1073](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ablink1073+updated%3A2024-01-11..2024-01-16&type=Issues)
-<!-- <END NEW CHANGELOG ENTRY> -->
-
## 2.12.4
([Full
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.12.3...7bb21b45392c889b5c87eb0d1b48662a497ba15a))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/PKG-INFO
new/jupyter_server-2.13.0/PKG-INFO
--- old/jupyter_server-2.12.5/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: jupyter_server
-Version: 2.12.5
+Version: 2.13.0
Summary: The backendâi.e. core services, APIs, and REST endpointsâto
Jupyter web applications.
Project-URL: Homepage, https://jupyter-server.readthedocs.io
Project-URL: Documentation, https://jupyter-server.readthedocs.io
@@ -92,7 +92,7 @@
Requires-Dist: ipykernel; extra == 'test'
Requires-Dist: pre-commit; extra == 'test'
Requires-Dist: pytest-console-scripts; extra == 'test'
-Requires-Dist: pytest-jupyter[server]>=0.4; extra == 'test'
+Requires-Dist: pytest-jupyter[server]>=0.7; extra == 'test'
Requires-Dist: pytest-timeout; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Requires-Dist: requests; extra == 'test'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/jupyter_server/_version.py
new/jupyter_server-2.13.0/jupyter_server/_version.py
--- old/jupyter_server-2.12.5/jupyter_server/_version.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/_version.py 2020-02-02
01:00:00.000000000 +0100
@@ -6,7 +6,7 @@
from typing import List
# Version string must appear intact for automatic versioning
-__version__ = "2.12.5"
+__version__ = "2.13.0"
# Build up version_info tuple for backwards compatibility
pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/auth/decorator.py
new/jupyter_server-2.13.0/jupyter_server/auth/decorator.py
--- old/jupyter_server-2.12.5/jupyter_server/auth/decorator.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/auth/decorator.py 2020-02-02
01:00:00.000000000 +0100
@@ -85,3 +85,58 @@
return cast(FuncT, wrapper(method))
return cast(FuncT, wrapper)
+
+
+def allow_unauthenticated(method: FuncT) -> FuncT:
+ """A decorator for tornado.web.RequestHandler methods
+ that allows any user to make the following request.
+
+ Selectively disables the 'authentication' layer of REST API which
+ is active when `ServerApp.allow_unauthenticated_access = False`.
+
+ To be used exclusively on endpoints which may be considered public,
+ for example the login page handler.
+
+ .. versionadded:: 2.13
+
+ Parameters
+ ----------
+ method : bound callable
+ the endpoint method to remove authentication from.
+ """
+
+ @wraps(method)
+ def wrapper(self, *args, **kwargs):
+ return method(self, *args, **kwargs)
+
+ setattr(wrapper, "__allow_unauthenticated", True)
+
+ return cast(FuncT, wrapper)
+
+
+def ws_authenticated(method: FuncT) -> FuncT:
+ """A decorator for websockets derived from `WebSocketHandler`
+ that authenticates user before allowing to proceed.
+
+ Differently from tornado.web.authenticated, does not redirect
+ to the login page, which would be meaningless for websockets.
+
+ .. versionadded:: 2.13
+
+ Parameters
+ ----------
+ method : bound callable
+ the endpoint method to add authentication for.
+ """
+
+ @wraps(method)
+ def wrapper(self, *args, **kwargs):
+ user = self.current_user
+ if user is None:
+ self.log.warning("Couldn't authenticate WebSocket connection")
+ raise HTTPError(403)
+ return method(self, *args, **kwargs)
+
+ setattr(wrapper, "__allow_unauthenticated", False)
+
+ return cast(FuncT, wrapper)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/jupyter_server/auth/login.py
new/jupyter_server-2.13.0/jupyter_server/auth/login.py
--- old/jupyter_server-2.12.5/jupyter_server/auth/login.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/auth/login.py 2020-02-02
01:00:00.000000000 +0100
@@ -9,6 +9,7 @@
from tornado.escape import url_escape
from ..base.handlers import JupyterHandler
+from .decorator import allow_unauthenticated
from .security import passwd_check, set_password
@@ -73,6 +74,7 @@
url = default
self.redirect(url)
+ @allow_unauthenticated
def get(self):
"""Get the login form."""
if self.current_user:
@@ -81,6 +83,7 @@
else:
self._render()
+ @allow_unauthenticated
def post(self):
"""Post a login."""
user = self.current_user =
self.identity_provider.process_login_form(self)
@@ -110,6 +113,7 @@
"""Check a passwd."""
return passwd_check(a, b)
+ @allow_unauthenticated
def post(self):
"""Post a login form."""
typed_password = self.get_argument("password", default="")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/jupyter_server/auth/logout.py
new/jupyter_server-2.13.0/jupyter_server/auth/logout.py
--- old/jupyter_server-2.12.5/jupyter_server/auth/logout.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/auth/logout.py 2020-02-02
01:00:00.000000000 +0100
@@ -3,11 +3,13 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from ..base.handlers import JupyterHandler
+from .decorator import allow_unauthenticated
class LogoutHandler(JupyterHandler):
"""An auth logout handler."""
+ @allow_unauthenticated
def get(self):
"""Handle a logout."""
self.identity_provider.clear_login_cookie(self)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/base/handlers.py
new/jupyter_server-2.13.0/jupyter_server/base/handlers.py
--- old/jupyter_server-2.12.5/jupyter_server/base/handlers.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/base/handlers.py 2020-02-02
01:00:00.000000000 +0100
@@ -14,7 +14,7 @@
import warnings
from http.client import responses
from logging import Logger
-from typing import TYPE_CHECKING, Any, Awaitable, Sequence, cast
+from typing import TYPE_CHECKING, Any, Awaitable, Coroutine, Sequence, cast
from urllib.parse import urlparse
import prometheus_client
@@ -29,7 +29,7 @@
from jupyter_server import CallContext
from jupyter_server._sysinfo import get_sys_info
from jupyter_server._tz import utcnow
-from jupyter_server.auth.decorator import authorized
+from jupyter_server.auth.decorator import allow_unauthenticated, authorized
from jupyter_server.auth.identity import User
from jupyter_server.i18n import combine_translations
from jupyter_server.services.security import csp_report_uri
@@ -589,7 +589,7 @@
)
return allow
- async def prepare(self) -> Awaitable[None] | None: # type:ignore[override]
+ async def prepare(self, *, _redirect_to_login=True) -> Awaitable[None] |
None: # type:ignore[override]
"""Prepare a response."""
# Set the current Jupyter Handler context variable.
CallContext.set(CallContext.JUPYTER_HANDLER, self)
@@ -630,6 +630,25 @@
self.set_cors_headers()
if self.request.method not in {"GET", "HEAD", "OPTIONS"}:
self.check_xsrf_cookie()
+
+ if not self.settings.get("allow_unauthenticated_access", False):
+ if not self.request.method:
+ raise HTTPError(403)
+ method = getattr(self, self.request.method.lower())
+ if not getattr(method, "__allow_unauthenticated", False):
+ if _redirect_to_login:
+ # reuse `web.authenticated` logic, which redirects to the
login
+ # page on GET and HEAD and otherwise raises 403
+ return web.authenticated(lambda _: super().prepare())(self)
+ else:
+ # raise 403 if user is not known without redirecting to
login page
+ user = self.current_user
+ if user is None:
+ self.log.warning(
+ f"Couldn't authenticate {self.__class__.__name__}
connection"
+ )
+ raise web.HTTPError(403)
+
return super().prepare()
# ---------------------------------------------------------------
@@ -726,7 +745,7 @@
class APIHandler(JupyterHandler):
"""Base class for API handlers"""
- async def prepare(self) -> None:
+ async def prepare(self) -> None: # type:ignore[override]
"""Prepare an API response."""
await super().prepare()
if not self.check_origin():
@@ -794,6 +813,7 @@
self.set_header("Content-Type", set_content_type)
return super().finish(*args, **kwargs)
+ @allow_unauthenticated
def options(self, *args: Any, **kwargs: Any) -> None:
"""Get the options."""
if "Access-Control-Allow-Headers" in self.settings.get("headers", {}):
@@ -837,7 +857,7 @@
class Template404(JupyterHandler):
"""Render our 404 template"""
- async def prepare(self) -> None:
+ async def prepare(self) -> None: # type:ignore[override]
"""Prepare a 404 response."""
await super().prepare()
raise web.HTTPError(404)
@@ -1002,6 +1022,18 @@
"""Compute the etag."""
return None
+ # access is allowed as this class is used to serve static assets on login
page
+ # TODO: create an allow-list of files used on login page and remove this
decorator
+ @allow_unauthenticated
+ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any,
None]:
+ return super().get(path, include_body)
+
+ # access is allowed as this class is used to serve static assets on login
page
+ # TODO: create an allow-list of files used on login page and remove this
decorator
+ @allow_unauthenticated
+ def head(self, path: str) -> Awaitable[None]:
+ return super().head(path)
+
@classmethod
def get_absolute_path(cls, roots: Sequence[str], path: str) -> str:
"""locate a file to serve on our static file search path"""
@@ -1036,6 +1068,7 @@
_track_activity = False
+ @allow_unauthenticated
def get(self) -> None:
"""Get the server version info."""
# not authenticated, so give as few info as possible
@@ -1048,6 +1081,7 @@
This should be the first, highest priority handler.
"""
+ @allow_unauthenticated
def get(self) -> None:
"""Handle trailing slashes in a get."""
assert self.request.uri is not None
@@ -1064,6 +1098,7 @@
class MainHandler(JupyterHandler):
"""Simple handler for base_url."""
+ @allow_unauthenticated
def get(self) -> None:
"""Get the main template."""
html = self.render_template("main.html")
@@ -1104,18 +1139,20 @@
self.log.debug("Redirecting %s to %s", self.request.path, url)
self.redirect(url)
+ @allow_unauthenticated
async def get(self, path: str = "") -> None:
return await self.redirect_to_files(self, path)
class RedirectWithParams(web.RequestHandler):
- """Sam as web.RedirectHandler, but preserves URL parameters"""
+ """Same as web.RedirectHandler, but preserves URL parameters"""
def initialize(self, url: str, permanent: bool = True) -> None:
"""Initialize a redirect handler."""
self._url = url
self._permanent = permanent
+ @allow_unauthenticated
def get(self) -> None:
"""Get a redirect."""
sep = "&" if "?" in self._url else "?"
@@ -1128,6 +1165,7 @@
Return prometheus metrics for this server
"""
+ @allow_unauthenticated
def get(self) -> None:
"""Get prometheus metrics."""
if self.settings["authenticate_prometheus"] and not self.logged_in:
@@ -1137,6 +1175,18 @@
self.write(prometheus_client.generate_latest(prometheus_client.REGISTRY))
+class PublicStaticFileHandler(web.StaticFileHandler):
+ """Same as web.StaticFileHandler, but decorated to acknowledge that auth
is not required."""
+
+ @allow_unauthenticated
+ def head(self, path: str) -> Awaitable[None]:
+ return super().head(path)
+
+ @allow_unauthenticated
+ def get(self, path: str, include_body: bool = True) -> Coroutine[Any, Any,
None]:
+ return super().get(path, include_body)
+
+
# -----------------------------------------------------------------------------
# URL pattern fragments for reuse
# -----------------------------------------------------------------------------
@@ -1152,6 +1202,6 @@
default_handlers = [
(r".*/", TrailingSlashHandler),
(r"api", APIVersionHandler),
- (r"/(robots\.txt|favicon\.ico)", web.StaticFileHandler),
+ (r"/(robots\.txt|favicon\.ico)", PublicStaticFileHandler),
(r"/metrics", PrometheusMetricsHandler),
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/base/websocket.py
new/jupyter_server-2.13.0/jupyter_server/base/websocket.py
--- old/jupyter_server-2.12.5/jupyter_server/base/websocket.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/base/websocket.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,11 +1,15 @@
"""Base websocket classes."""
import re
+import warnings
from typing import Optional, no_type_check
from urllib.parse import urlparse
-from tornado import ioloop
+from tornado import ioloop, web
from tornado.iostream import IOStream
+from jupyter_server.base.handlers import JupyterHandler
+from jupyter_server.utils import JupyterServerAuthWarning
+
# ping interval for keeping websockets alive (30 seconds)
WS_PING_INTERVAL = 30000
@@ -83,6 +87,40 @@
"""meaningless for websockets"""
@no_type_check
+ def _maybe_auth(self):
+ """Verify authentication if required.
+
+ Only used when the websocket class does not inherit from
JupyterHandler.
+ """
+ if not self.settings.get("allow_unauthenticated_access", False):
+ if not self.request.method:
+ raise web.HTTPError(403)
+ method = getattr(self, self.request.method.lower())
+ if not getattr(method, "__allow_unauthenticated", False):
+ # rather than re-using `web.authenticated` which also redirects
+ # to login page on GET, just raise 403 if user is not known
+ user = self.current_user
+ if user is None:
+ self.log.warning("Couldn't authenticate WebSocket
connection")
+ raise web.HTTPError(403)
+
+ @no_type_check
+ def prepare(self, *args, **kwargs):
+ """Handle a get request."""
+ if not isinstance(self, JupyterHandler):
+ should_authenticate = not
self.settings.get("allow_unauthenticated_access", False)
+ if "identity_provider" in self.settings and should_authenticate:
+ warnings.warn(
+ "WebSocketMixin sub-class does not inherit from
JupyterHandler"
+ " preventing proper authentication using custom identity
provider.",
+ JupyterServerAuthWarning,
+ stacklevel=2,
+ )
+ self._maybe_auth()
+ return super().prepare(*args, **kwargs)
+ return super().prepare(*args, **kwargs, _redirect_to_login=False)
+
+ @no_type_check
def open(self, *args, **kwargs):
"""Open the websocket."""
self.log.debug("Opening websocket %s", self.request.path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/extension/application.py
new/jupyter_server-2.13.0/jupyter_server/extension/application.py
--- old/jupyter_server-2.12.5/jupyter_server/extension/application.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/extension/application.py
2020-02-02 01:00:00.000000000 +0100
@@ -358,7 +358,7 @@
)
new_handlers.append(handler)
- webapp.add_handlers(".*$", new_handlers) # type:ignore[arg-type]
+ webapp.add_handlers(".*$", new_handlers)
def _prepare_templates(self):
"""Add templates to web app settings if extension has templates."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/extension/utils.py
new/jupyter_server-2.13.0/jupyter_server/extension/utils.py
--- old/jupyter_server-2.12.5/jupyter_server/extension/utils.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/extension/utils.py 2020-02-02
01:00:00.000000000 +0100
@@ -110,7 +110,7 @@
hook or metadata field.
An extension is valid if:
1) name is an importable Python package.
- 1) the package has a _jupyter_server_extension_paths function
+ 1) the package has a _jupyter_server_extension_points function
2) each extension path has a _load_jupyter_server_extension function
If this works, nothing should happen.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/jupyter_server/serverapp.py
new/jupyter_server-2.13.0/jupyter_server/serverapp.py
--- old/jupyter_server-2.12.5/jupyter_server/serverapp.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/serverapp.py 2020-02-02
01:00:00.000000000 +0100
@@ -41,10 +41,19 @@
from tornado.httputil import url_concat
from tornado.log import LogFormatter, access_log, app_log, gen_log
from tornado.netutil import bind_sockets
+from tornado.routing import Matcher, Rule
if not sys.platform.startswith("win"):
from tornado.netutil import bind_unix_socket
+if sys.platform.startswith("win"):
+ try:
+ import colorama
+
+ colorama.init()
+ except ImportError:
+ pass
+
from traitlets import (
Any,
Bool,
@@ -115,6 +124,7 @@
)
from jupyter_server.services.sessions.sessionmanager import SessionManager
from jupyter_server.utils import (
+ JupyterServerAuthWarning,
check_pid,
fetch,
unix_socket_in_use,
@@ -240,6 +250,8 @@
authorizer=None,
identity_provider=None,
kernel_websocket_connection_class=None,
+ websocket_ping_interval=None,
+ websocket_ping_timeout=None,
):
"""Initialize a server web application."""
if identity_provider is None:
@@ -255,7 +267,7 @@
warnings.warn(
"authorizer unspecified. Using permissive AllowAllAuthorizer."
" Specify an authorizer to avoid this message.",
- RuntimeWarning,
+ JupyterServerAuthWarning,
stacklevel=2,
)
authorizer = AllowAllAuthorizer(parent=jupyter_app,
identity_provider=identity_provider)
@@ -277,11 +289,54 @@
authorizer=authorizer,
identity_provider=identity_provider,
kernel_websocket_connection_class=kernel_websocket_connection_class,
+ websocket_ping_interval=websocket_ping_interval,
+ websocket_ping_timeout=websocket_ping_timeout,
)
handlers = self.init_handlers(default_services, settings)
+ undecorated_methods = []
+ for matcher, handler, *_ in handlers:
+ undecorated_methods.extend(self._check_handler_auth(matcher,
handler))
+
+ if undecorated_methods:
+ message = (
+ "Core endpoints without @allow_unauthenticated,
@ws_authenticated, nor @web.authenticated:\n"
+ + "\n".join(undecorated_methods)
+ )
+ if jupyter_app.allow_unauthenticated_access:
+ warnings.warn(
+ message,
+ JupyterServerAuthWarning,
+ stacklevel=2,
+ )
+ else:
+ raise Exception(message)
+
super().__init__(handlers, **settings)
+ def add_handlers(self, host_pattern, host_handlers):
+ undecorated_methods = []
+ for rule in host_handlers:
+ if isinstance(rule, Rule):
+ matcher = rule.matcher
+ handler = rule.target
+ else:
+ matcher, handler, *_ = rule
+ undecorated_methods.extend(self._check_handler_auth(matcher,
handler))
+
+ if undecorated_methods and not
self.settings["allow_unauthenticated_access"]:
+ message = (
+ "Extension endpoints without @allow_unauthenticated,
@ws_authenticated, nor @web.authenticated:\n"
+ + "\n".join(undecorated_methods)
+ )
+ warnings.warn(
+ message,
+ JupyterServerAuthWarning,
+ stacklevel=2,
+ )
+
+ return super().add_handlers(host_pattern, host_handlers)
+
def init_settings(
self,
jupyter_app,
@@ -301,6 +356,8 @@
authorizer=None,
identity_provider=None,
kernel_websocket_connection_class=None,
+ websocket_ping_interval=None,
+ websocket_ping_timeout=None,
):
"""Initialize settings for the web application."""
_template_path = settings_overrides.get(
@@ -370,6 +427,7 @@
"login_url": url_path_join(base_url, "/login"),
"xsrf_cookies": True,
"disable_check_xsrf": jupyter_app.disable_check_xsrf,
+ "allow_unauthenticated_access":
jupyter_app.allow_unauthenticated_access,
"allow_remote_access": jupyter_app.allow_remote_access,
"local_hostnames": jupyter_app.local_hostnames,
"authenticate_prometheus": jupyter_app.authenticate_prometheus,
@@ -383,6 +441,8 @@
"identity_provider": identity_provider,
"event_logger": event_logger,
"kernel_websocket_connection_class":
kernel_websocket_connection_class,
+ "websocket_ping_interval": websocket_ping_interval,
+ "websocket_ping_timeout": websocket_ping_timeout,
# handlers
"extra_services": extra_services,
# Jupyter stuff
@@ -486,6 +546,40 @@
sources.extend(self.settings["last_activity_times"].values())
return max(sources)
+ def _check_handler_auth(
+ self, matcher: t.Union[str, Matcher], handler: type[web.RequestHandler]
+ ):
+ missing_authentication = []
+ for method_name in handler.SUPPORTED_METHODS:
+ method = getattr(handler, method_name.lower())
+ is_unimplemented = method ==
web.RequestHandler._unimplemented_method
+ is_allowlisted = hasattr(method, "__allow_unauthenticated")
+ is_blocklisted = _has_tornado_web_authenticated(method)
+ if not is_unimplemented and not is_allowlisted and not
is_blocklisted:
+ missing_authentication.append(
+ f"- {method_name} of {handler.__name__} registered for
{matcher}"
+ )
+ return missing_authentication
+
+
+def _has_tornado_web_authenticated(method: t.Callable[..., t.Any]) -> bool:
+ """Check if given method was decorated with @web.authenticated.
+
+ Note: it is ok if we reject on @authorized @web.authenticated
+ because the correct order is @web.authenticated @authorized.
+ """
+ if not hasattr(method, "__wrapped__"):
+ return False
+ if not hasattr(method, "__code__"):
+ return False
+ code = method.__code__
+ if hasattr(code, "co_qualname"):
+ # new in 3.11
+ return code.co_qualname.startswith("authenticated") #
type:ignore[no-any-return]
+ elif hasattr(code, "co_filename"):
+ return code.co_filename.replace("\\", "/").endswith("tornado/web.py")
+ return False
+
class JupyterPasswordApp(JupyterApp):
"""Set a password for the Jupyter server.
@@ -1214,6 +1308,33 @@
""",
)
+ _allow_unauthenticated_access_env =
"JUPYTER_SERVER_ALLOW_UNAUTHENTICATED_ACCESS"
+
+ allow_unauthenticated_access = Bool(
+ True,
+ config=True,
+ help=f"""Allow unauthenticated access to endpoints without
authentication rule.
+
+ When set to `True` (default in jupyter-server 2.0, subject to change
+ in the future), any request to an endpoint without an authentication
rule
+ (either `@tornado.web.authenticated`, or `@allow_unauthenticated`)
+ will be permitted, regardless of whether user has logged in or not.
+
+ When set to `False`, logging in will be required for access to each
endpoint,
+ excluding the endpoints marked with `@allow_unauthenticated` decorator.
+
+ This option can be configured using
`{_allow_unauthenticated_access_env}`
+ environment variable: any non-empty value other than "true" and "yes"
will
+ prevent unauthenticated access to endpoints without
`@allow_unauthenticated`.
+ """,
+ )
+
+ @default("allow_unauthenticated_access")
+ def _allow_unauthenticated_access_default(self):
+ if os.getenv(self._allow_unauthenticated_access_env):
+ return os.environ[self._allow_unauthenticated_access_env].lower()
in ["true", "yes"]
+ return True
+
allow_remote_access = Bool(
config=True,
help="""Allow requests where the Host header doesn't point to a local
server
@@ -1515,6 +1636,32 @@
return
"jupyter_server.gateway.connections.GatewayWebSocketConnection"
return ZMQChannelsWebsocketConnection
+ websocket_ping_interval = Integer(
+ config=True,
+ help="""
+ Configure the websocket ping interval in seconds.
+
+ Websockets are long-lived connections that are used by some Jupyter
+ Server extensions.
+
+ Periodic pings help to detect disconnected clients and keep the
+ connection active. If this is set to None, then no pings will be
+ performed.
+
+ When a ping is sent, the client has ``websocket_ping_timeout``
+ seconds to respond. If no response is received within this period,
+ the connection will be closed from the server side.
+ """,
+ )
+ websocket_ping_timeout = Integer(
+ config=True,
+ help="""
+ Configure the websocket ping timeout in seconds.
+
+ See ``websocket_ping_interval`` for details.
+ """,
+ )
+
config_manager_class = Type(
default_value=ConfigManager,
config=True,
@@ -1706,7 +1853,9 @@
preferred_dir = Unicode(
config=True,
- help=trans.gettext("Preferred starting directory to use for notebooks
and kernels."),
+ help=trans.gettext(
+ "Preferred starting directory to use for notebooks and kernels.
ServerApp.preferred_dir is deprecated in jupyter-server 2.0. Use
FileContentsManager.preferred_dir instead"
+ ),
)
@default("preferred_dir")
@@ -2101,6 +2250,8 @@
authorizer=self.authorizer,
identity_provider=self.identity_provider,
kernel_websocket_connection_class=self.kernel_websocket_connection_class,
+ websocket_ping_interval=self.websocket_ping_interval,
+ websocket_ping_timeout=self.websocket_ping_timeout,
)
if self.certfile:
self.ssl_options["certfile"] = self.certfile
@@ -2268,7 +2419,7 @@
info(self.running_server_info())
yes = _i18n("y")
no = _i18n("n")
- sys.stdout.write(_i18n("Shutdown this Jupyter server (%s/[%s])? ") %
(yes, no))
+ sys.stdout.write(_i18n("Shut down this Jupyter server (%s/[%s])? ") %
(yes, no))
sys.stdout.flush()
r, w, x = select.select([sys.stdin], [], [], 5)
if r:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/services/api/handlers.py
new/jupyter_server-2.13.0/jupyter_server/services/api/handlers.py
--- old/jupyter_server-2.12.5/jupyter_server/services/api/handlers.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/services/api/handlers.py
2020-02-02 01:00:00.000000000 +0100
@@ -27,6 +27,11 @@
@web.authenticated
@authorized
+ def head(self):
+ return self.get("api.yaml", include_body=False)
+
+ @web.authenticated
+ @authorized
def get(self):
"""Get the API spec."""
self.log.warning("Serving api spec (experimental, incomplete)")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/services/contents/handlers.py
new/jupyter_server-2.13.0/jupyter_server/services/contents/handlers.py
--- old/jupyter_server-2.12.5/jupyter_server/services/contents/handlers.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/services/contents/handlers.py
2020-02-02 01:00:00.000000000 +0100
@@ -16,7 +16,7 @@
from jupyter_core.utils import ensure_async
from tornado import web
-from jupyter_server.auth.decorator import authorized
+from jupyter_server.auth.decorator import allow_unauthenticated, authorized
from jupyter_server.base.handlers import APIHandler, JupyterHandler, path_regex
from jupyter_server.utils import url_escape, url_path_join
@@ -400,6 +400,7 @@
"DELETE",
) # type:ignore[assignment]
+ @allow_unauthenticated
def get(self, path):
"""Handle a notebooks redirect."""
self.log.warning("/api/notebooks is deprecated, use /api/contents")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/services/events/handlers.py
new/jupyter_server-2.13.0/jupyter_server/services/events/handlers.py
--- old/jupyter_server-2.12.5/jupyter_server/services/events/handlers.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/services/events/handlers.py
2020-02-02 01:00:00.000000000 +0100
@@ -12,7 +12,7 @@
from jupyter_core.utils import ensure_async
from tornado import web, websocket
-from jupyter_server.auth.decorator import authorized
+from jupyter_server.auth.decorator import authorized, ws_authenticated
from jupyter_server.base.handlers import JupyterHandler
from ...base.handlers import APIHandler
@@ -29,16 +29,11 @@
auth_resource = AUTH_RESOURCE
async def pre_get(self):
- """Handles authentication/authorization when
+ """Handles authorization when
attempting to subscribe to events emitted by
Jupyter Server's eventbus.
"""
- # authenticate the request before opening the websocket
user = self.current_user
- if user is None:
- self.log.warning("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
-
# authorize the user.
authorized = await ensure_async(
self.authorizer.is_authorized(self, user, "execute", "events")
@@ -46,6 +41,7 @@
if not authorized:
raise web.HTTPError(403)
+ @ws_authenticated
async def get(self, *args, **kwargs):
"""Get an event socket."""
await ensure_async(self.pre_get())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/jupyter_server/services/kernels/websocket.py
new/jupyter_server-2.13.0/jupyter_server/services/kernels/websocket.py
--- old/jupyter_server-2.12.5/jupyter_server/services/kernels/websocket.py
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/services/kernels/websocket.py
2020-02-02 01:00:00.000000000 +0100
@@ -6,6 +6,7 @@
from tornado import web
from tornado.websocket import WebSocketHandler
+from jupyter_server.auth.decorator import ws_authenticated
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.base.websocket import WebSocketMixin
@@ -34,11 +35,7 @@
async def pre_get(self):
"""Handle a pre_get."""
- # authenticate first
user = self.current_user
- if user is None:
- self.log.warning("Couldn't authenticate WebSocket connection")
- raise web.HTTPError(403)
# authorize the user.
authorized = await ensure_async(
@@ -61,6 +58,7 @@
if hasattr(self.connection, "prepare"):
await self.connection.prepare()
+ @ws_authenticated
async def get(self, kernel_id):
"""Handle a get request for a kernel."""
self.kernel_id = kernel_id
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/jupyter_server/utils.py
new/jupyter_server-2.13.0/jupyter_server/utils.py
--- old/jupyter_server-2.12.5/jupyter_server/utils.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/jupyter_server/utils.py 2020-02-02
01:00:00.000000000 +0100
@@ -439,3 +439,11 @@
else:
# called with un-dotted string
return __import__(parts[0])
+
+
+class JupyterServerAuthWarning(RuntimeWarning):
+ """Emitted when authentication configuration issue is detected.
+
+ Intended for filtering out expected warnings in tests, including
+ downstream tests, rather than for users to silence this warning.
+ """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/pyproject.toml
new/jupyter_server-2.13.0/pyproject.toml
--- old/jupyter_server-2.12.5/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
+++ new/jupyter_server-2.13.0/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
@@ -56,7 +56,7 @@
"ipykernel",
"pytest-console-scripts",
"pytest-timeout",
- "pytest-jupyter[server]>=0.4",
+ "pytest-jupyter[server]>=0.7",
"pytest>=7.0",
"requests",
"pre-commit",
@@ -241,6 +241,7 @@
"error",
"ignore:datetime.datetime.utc:DeprecationWarning",
"module:add_callback_from_signal is deprecated:DeprecationWarning",
+ "ignore::jupyter_server.utils.JupyterServerAuthWarning"
]
[tool.coverage.report]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/tests/base/test_handlers.py
new/jupyter_server-2.13.0/tests/base/test_handlers.py
--- old/jupyter_server-2.12.5/tests/base/test_handlers.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/tests/base/test_handlers.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,12 +1,15 @@
"""Test Base Handlers"""
import os
import warnings
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
+import pytest
+from tornado.httpclient import HTTPClientError
from tornado.httpserver import HTTPRequest
from tornado.httputil import HTTPHeaders
-from jupyter_server.auth import AllowAllAuthorizer, IdentityProvider
+from jupyter_server.auth import AllowAllAuthorizer, IdentityProvider, User
+from jupyter_server.auth.decorator import allow_unauthenticated
from jupyter_server.base.handlers import (
APIHandler,
APIVersionHandler,
@@ -18,6 +21,7 @@
RedirectWithParams,
)
from jupyter_server.serverapp import ServerApp
+from jupyter_server.utils import url_path_join
def test_authenticated_handler(jp_serverapp):
@@ -61,6 +65,134 @@
assert handler.check_referer() is True
+class NoAuthRulesHandler(JupyterHandler):
+ def options(self) -> None:
+ self.finish({})
+
+ def get(self) -> None:
+ self.finish({})
+
+
+class PermissiveHandler(JupyterHandler):
+ @allow_unauthenticated
+ def options(self) -> None:
+ self.finish({})
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access": True}}]
+)
+async def test_jupyter_handler_auth_permissive(jp_serverapp, jp_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [
+ (url_path_join(app.base_url, "no-rules"), NoAuthRulesHandler),
+ (url_path_join(app.base_url, "permissive"), PermissiveHandler),
+ ],
+ )
+
+ # should always permit access when `@allow_unauthenticated` is used
+ res = await jp_fetch("permissive", method="OPTIONS",
headers={"Authorization": ""})
+ assert res.code == 200
+
+ # should allow access when no authentication rules are set up
+ res = await jp_fetch("no-rules", method="OPTIONS",
headers={"Authorization": ""})
+ assert res.code == 200
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access":
False}}]
+)
+async def test_jupyter_handler_auth_required(jp_serverapp, jp_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [
+ (url_path_join(app.base_url, "no-rules"), NoAuthRulesHandler),
+ (url_path_join(app.base_url, "permissive"), PermissiveHandler),
+ ],
+ )
+
+ # should always permit access when `@allow_unauthenticated` is used
+ res = await jp_fetch("permissive", method="OPTIONS",
headers={"Authorization": ""})
+ assert res.code == 200
+
+ # should forbid access when no authentication rules are set up:
+ # - by redirecting to login page for GET and HEAD
+ res = await jp_fetch(
+ "no-rules",
+ method="GET",
+ headers={"Authorization": ""},
+ follow_redirects=False,
+ raise_error=False,
+ )
+ assert res.code == 302
+ assert "/login" in res.headers["Location"]
+
+ # - by returning 403 immediately for other requests
+ with pytest.raises(HTTPClientError) as exception:
+ res = await jp_fetch("no-rules", method="OPTIONS",
headers={"Authorization": ""})
+ assert exception.value.code == 403
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access":
False}}]
+)
+async def test_jupyter_handler_auth_calls_prepare(jp_serverapp, jp_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [
+ (url_path_join(app.base_url, "no-rules"), NoAuthRulesHandler),
+ (url_path_join(app.base_url, "permissive"), PermissiveHandler),
+ ],
+ )
+
+ # should call `super.prepare()` in `@allow_unauthenticated` code path
+ with patch.object(JupyterHandler, "prepare", return_value=None) as mock:
+ res = await jp_fetch("permissive", method="OPTIONS")
+ assert res.code == 200
+ assert mock.call_count == 1
+
+ # should call `super.prepare()` in code path that checks authentication
+ with patch.object(JupyterHandler, "prepare", return_value=None) as mock:
+ res = await jp_fetch("no-rules", method="OPTIONS")
+ assert res.code == 200
+ assert mock.call_count == 1
+
+
+class IndiscriminateIdentityProvider(IdentityProvider):
+ async def get_user(self, handler):
+ return User(username="test")
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access":
False}}]
+)
+async def test_jupyter_handler_auth_respsects_identity_provider(jp_serverapp,
jp_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [(url_path_join(app.base_url, "no-rules"), NoAuthRulesHandler)],
+ )
+
+ def fetch():
+ return jp_fetch("no-rules", method="OPTIONS",
headers={"Authorization": ""})
+
+ # If no identity provider is set the following request should fail
+ # because the default tornado user would not be found:
+ with pytest.raises(HTTPClientError) as exception:
+ await fetch()
+ assert exception.value.code == 403
+
+ iidp = IndiscriminateIdentityProvider()
+ # should allow access with the user set be the identity provider
+ with patch.dict(jp_serverapp.web_app.settings, {"identity_provider":
iidp}):
+ res = await fetch()
+ assert res.code == 200
+
+
def test_api_handler(jp_serverapp):
app: ServerApp = jp_serverapp
headers = HTTPHeaders({"Origin": "foo"})
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/tests/base/test_websocket.py
new/jupyter_server-2.13.0/tests/base/test_websocket.py
--- old/jupyter_server-2.12.5/tests/base/test_websocket.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/tests/base/test_websocket.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,15 +1,20 @@
"""Test Base Websocket classes"""
import logging
import time
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
import pytest
+from tornado.httpclient import HTTPClientError
from tornado.httpserver import HTTPRequest
from tornado.httputil import HTTPHeaders
from tornado.websocket import WebSocketClosedError, WebSocketHandler
+from jupyter_server.auth import IdentityProvider, User
+from jupyter_server.auth.decorator import allow_unauthenticated
+from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.base.websocket import WebSocketMixin
from jupyter_server.serverapp import ServerApp
+from jupyter_server.utils import JupyterServerAuthWarning, url_path_join
class MockHandler(WebSocketMixin, WebSocketHandler):
@@ -60,3 +65,126 @@
mixin.send_ping()
with pytest.raises(WebSocketClosedError):
mixin.write_message("hello")
+
+
+class MockJupyterHandler(MockHandler, JupyterHandler):
+ pass
+
+
+class NoAuthRulesWebsocketHandler(MockJupyterHandler):
+ pass
+
+
+class PermissiveWebsocketHandler(MockJupyterHandler):
+ @allow_unauthenticated
+ def get(self, *args, **kwargs) -> None:
+ return super().get(*args, **kwargs)
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access": True}}]
+)
+async def test_websocket_auth_permissive(jp_serverapp, jp_ws_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [
+ (url_path_join(app.base_url, "no-rules"),
NoAuthRulesWebsocketHandler),
+ (url_path_join(app.base_url, "permissive"),
PermissiveWebsocketHandler),
+ ],
+ )
+
+ # should always permit access when `@allow_unauthenticated` is used
+ ws = await jp_ws_fetch("permissive", headers={"Authorization": ""})
+ ws.close()
+
+ # should allow access when no authentication rules are set up
+ ws = await jp_ws_fetch("no-rules", headers={"Authorization": ""})
+ ws.close()
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access":
False}}]
+)
+async def test_websocket_auth_required(jp_serverapp, jp_ws_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [
+ (url_path_join(app.base_url, "no-rules"),
NoAuthRulesWebsocketHandler),
+ (url_path_join(app.base_url, "permissive"),
PermissiveWebsocketHandler),
+ ],
+ )
+
+ # should always permit access when `@allow_unauthenticated` is used
+ ws = await jp_ws_fetch("permissive", headers={"Authorization": ""})
+ ws.close()
+
+ # should forbid access when no authentication rules are set up
+ with pytest.raises(HTTPClientError) as exception:
+ ws = await jp_ws_fetch("no-rules", headers={"Authorization": ""})
+ assert exception.value.code == 403
+
+
+class IndiscriminateIdentityProvider(IdentityProvider):
+ async def get_user(self, handler):
+ return User(username="test")
+
+
[email protected](
+ "jp_server_config", [{"ServerApp": {"allow_unauthenticated_access":
False}}]
+)
+async def test_websocket_auth_respsects_identity_provider(jp_serverapp,
jp_ws_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [(url_path_join(app.base_url, "no-rules"),
NoAuthRulesWebsocketHandler)],
+ )
+
+ def fetch():
+ return jp_ws_fetch("no-rules", headers={"Authorization": ""})
+
+ # If no identity provider is set the following request should fail
+ # because the default tornado user would not be found:
+ with pytest.raises(HTTPClientError) as exception:
+ await fetch()
+ assert exception.value.code == 403
+
+ iidp = IndiscriminateIdentityProvider()
+ # should allow access with the user set be the identity provider
+ with patch.dict(jp_serverapp.web_app.settings, {"identity_provider":
iidp}):
+ ws = await fetch()
+ ws.close()
+
+
+class PermissivePlainWebsocketHandler(MockHandler):
+ # note: inherits from MockHandler not MockJupyterHandler
+ @allow_unauthenticated
+ def get(self, *args, **kwargs) -> None:
+ return super().get(*args, **kwargs)
+
+
[email protected](
+ "jp_server_config",
+ [
+ {
+ "ServerApp": {
+ "allow_unauthenticated_access": False,
+ "identity_provider": IndiscriminateIdentityProvider(),
+ }
+ }
+ ],
+)
+async def test_websocket_auth_warns_mixin_lacks_jupyter_handler(jp_serverapp,
jp_ws_fetch):
+ app: ServerApp = jp_serverapp
+ app.web_app.add_handlers(
+ ".*$",
+ [(url_path_join(app.base_url, "permissive"),
PermissivePlainWebsocketHandler)],
+ )
+
+ with pytest.warns(
+ JupyterServerAuthWarning,
+ match="WebSocketMixin sub-class does not inherit from JupyterHandler",
+ ):
+ ws = await jp_ws_fetch("permissive", headers={"Authorization": ""})
+ ws.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/jupyter_server-2.12.5/tests/extension/test_handler.py
new/jupyter_server-2.13.0/tests/extension/test_handler.py
--- old/jupyter_server-2.12.5/tests/extension/test_handler.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/tests/extension/test_handler.py 2020-02-02
01:00:00.000000000 +0100
@@ -24,6 +24,47 @@
"jp_server_config",
[
{
+ "ServerApp": {
+ "allow_unauthenticated_access": False,
+ "jpserver_extensions": {"tests.extension.mockextensions":
True},
+ }
+ }
+ ],
+)
+async def test_handler_gets_blocked(jp_fetch, jp_server_config):
+ # should redirect to login page if authorization token is missing
+ r = await jp_fetch(
+ "mock",
+ method="GET",
+ headers={"Authorization": ""},
+ follow_redirects=False,
+ raise_error=False,
+ )
+ assert r.code == 302
+ assert "/login" in r.headers["Location"]
+ # should still work if authorization token is present
+ r = await jp_fetch("mock", method="GET")
+ assert r.code == 200
+
+
+def test_serverapp_warns_of_unauthenticated_handler(jp_configurable_serverapp):
+ # should warn about the handler missing decorator when unauthenticated
access forbidden
+ expected_warning = "Extension endpoints without @allow_unauthenticated,
@ws_authenticated, nor @web.authenticated:"
+ with pytest.warns(RuntimeWarning, match=expected_warning) as record:
+ jp_configurable_serverapp(allow_unauthenticated_access=False)
+ assert any(
+ [
+ "GET of MockExtensionTemplateHandler registered for
/a%40b/mock_template"
+ in r.message.args[0]
+ for r in record
+ ]
+ )
+
+
[email protected](
+ "jp_server_config",
+ [
+ {
"ServerApp": {"jpserver_extensions":
{"tests.extension.mockextensions": True}},
"MockExtensionApp": {
# Change a trait in the MockExtensionApp using
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/jupyter_server-2.12.5/tests/test_serverapp.py
new/jupyter_server-2.13.0/tests/test_serverapp.py
--- old/jupyter_server-2.12.5/tests/test_serverapp.py 2020-02-02
01:00:00.000000000 +0100
+++ new/jupyter_server-2.13.0/tests/test_serverapp.py 2020-02-02
01:00:00.000000000 +0100
@@ -9,16 +9,19 @@
import pytest
from jupyter_core.application import NoStart
+from tornado import web
from traitlets import TraitError
from traitlets.config import Config
from traitlets.tests.utils import check_help_all_output
+from jupyter_server.auth.decorator import allow_unauthenticated, authorized
from jupyter_server.auth.security import passwd_check
from jupyter_server.serverapp import (
JupyterPasswordApp,
JupyterServerListApp,
ServerApp,
ServerWebApplication,
+ _has_tornado_web_authenticated,
list_running_servers,
random_ports,
)
@@ -163,6 +166,26 @@
passwd_check(sv.identity_provider.hashed_password, password)
[email protected](
+ "env,expected",
+ [
+ ["yes", True],
+ ["Yes", True],
+ ["True", True],
+ ["true", True],
+ ["TRUE", True],
+ ["no", False],
+ ["nooo", False],
+ ["FALSE", False],
+ ["false", False],
+ ],
+)
+def test_allow_unauthenticated_env_var(jp_configurable_serverapp, env,
expected):
+ with patch.dict("os.environ",
{"JUPYTER_SERVER_ALLOW_UNAUTHENTICATED_ACCESS": env}):
+ app = jp_configurable_serverapp()
+ assert app.allow_unauthenticated_access == expected
+
+
def test_list_running_servers(jp_serverapp, jp_web_app):
servers = list(list_running_servers(jp_serverapp.runtime_dir))
assert len(servers) >= 1
@@ -617,3 +640,22 @@
serverapp.init_configurables()
serverapp.init_webapp()
assert serverapp.web_app.settings["static_immutable_cache"] ==
["/test/immutable"]
+
+
+def test():
+ pass
+
+
[email protected](
+ "method, expected",
+ [
+ [test, False],
+ [allow_unauthenticated(test), False],
+ [authorized(test), False],
+ [web.authenticated(test), True],
+ [web.authenticated(authorized(test)), True],
+ [authorized(web.authenticated(test)), False], # wrong order!
+ ],
+)
+def test_tornado_authentication_detection(method, expected):
+ assert _has_tornado_web_authenticated(method) == expected