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

Reply via email to