Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-jupyter-lsp for openSUSE:Factory checked in at 2024-01-19 23:02:02 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-jupyter-lsp (Old) and /work/SRC/openSUSE:Factory/.python-jupyter-lsp.new.16006 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-jupyter-lsp" Fri Jan 19 23:02:02 2024 rev:4 rq:1139869 version:2.2.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-jupyter-lsp/python-jupyter-lsp.changes 2023-12-17 21:35:17.064036317 +0100 +++ /work/SRC/openSUSE:Factory/.python-jupyter-lsp.new.16006/python-jupyter-lsp.changes 2024-01-19 23:02:20.842360664 +0100 @@ -1,0 +2,14 @@ +Fri Jan 19 08:16:17 UTC 2024 - Ben Greiner <c...@bnavigator.de> + +- Update to 2.2.2 + * address warning about renamed extension_points (#1035) + * fix compatibility with jupyter server 1.x + * fix an authentication-related security vulnerability + GHSA-4qhp-652w-c22x CVE-2024-22415 boo#1218976 + * add authorization support (lsp resource, jupyter-server v2+ + only) - this allows server operators for fine grained access + control, e.g. in case if specific users (such as guest or + read-only users) should not be allowed to access LSP; this is + in addition to authentication fixes + +------------------------------------------------------------------- Old: ---- jupyter-lsp-2.2.1.tar.gz New: ---- jupyter-lsp-2.2.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-jupyter-lsp.spec ++++++ --- /var/tmp/diff_new_pack.ty71oO/_old 2024-01-19 23:02:21.386380565 +0100 +++ /var/tmp/diff_new_pack.ty71oO/_new 2024-01-19 23:02:21.386380565 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-jupyter-lsp # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -16,13 +16,13 @@ # -%define shortversion 2.2.1 +%define shortversion 2.2.2 Name: python-jupyter-lsp -Version: 2.2.1 +Version: 2.2.2 Release: 0 Summary: LSP for Jupyter Notebook/Lab server License: BSD-3-Clause -URL: https://github.com/jupyter-lsp/jupyterlab-lsp/tree/master/python_packages/jupyter_lsp +URL: https://github.com/jupyter-lsp/jupyterlab-lsp/tree/main/python_packages/jupyter_lsp Source: https://files.pythonhosted.org/packages/source/j/jupyter-lsp/jupyter-lsp-%{version}.tar.gz BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} ++++++ jupyter-lsp-2.2.1.tar.gz -> jupyter-lsp-2.2.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/PKG-INFO new/jupyter-lsp-2.2.2/PKG-INFO --- old/jupyter-lsp-2.2.1/PKG-INFO 2023-11-26 21:29:20.138566300 +0100 +++ new/jupyter-lsp-2.2.2/PKG-INFO 2024-01-17 23:02:02.036046700 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jupyter-lsp -Version: 2.2.1 +Version: 2.2.2 Summary: Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server Author: jupyter-lsp Contributors Author-email: project.jupy...@gmail.com diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/__init__.py new/jupyter-lsp-2.2.2/jupyter_lsp/__init__.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/__init__.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/__init__.py 2024-01-17 22:58:59.000000000 +0100 @@ -12,3 +12,6 @@ def _jupyter_server_extension_paths(): return [{"module": "jupyter_lsp"}] + + +_jupyter_server_extension_points = _jupyter_server_extension_paths diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/_version.py new/jupyter-lsp-2.2.2/jupyter_lsp/_version.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/_version.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/_version.py 2024-01-17 22:58:59.000000000 +0100 @@ -1,3 +1,3 @@ """ single source of truth for jupyter_lsp version """ -__version__ = "2.2.1" +__version__ = "2.2.2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/handlers.py new/jupyter-lsp-2.2.2/jupyter_lsp/handlers.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/handlers.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/handlers.py 2024-01-17 22:58:59.000000000 +0100 @@ -2,14 +2,32 @@ """ from typing import Optional, Text +from jupyter_core.utils import ensure_async from jupyter_server.base.handlers import APIHandler -from jupyter_server.base.zmqhandlers import WebSocketHandler, WebSocketMixin from jupyter_server.utils import url_path_join as ujoin +from tornado import web +from tornado.websocket import WebSocketHandler + +try: + from jupyter_server.auth.decorator import authorized +except ImportError: + + def authorized(method): # type: ignore + """A no-op fallback for `jupyter_server 1.x`""" + return method + + +try: + from jupyter_server.base.websocket import WebSocketMixin +except ImportError: + from jupyter_server.base.zmqhandlers import WebSocketMixin from .manager import LanguageServerManager from .schema import SERVERS_RESPONSE from .specs.utils import censored_spec +AUTH_RESOURCE = "lsp" + class BaseHandler(APIHandler): manager = None # type: LanguageServerManager @@ -21,10 +39,43 @@ class LanguageServerWebSocketHandler( # type: ignore WebSocketMixin, WebSocketHandler, BaseHandler ): - """Setup tornado websocket to route to language server sessions""" + """Setup tornado websocket to route to language server sessions. + + The logic of `get` and `pre_get` methods is derived from jupyter-server ws handlers, + and should be kept in sync to follow best practice established by upstream; see: + https://github.com/jupyter-server/jupyter_server/blob/v2.12.5/jupyter_server/services/kernels/websocket.py#L36 + """ + + auth_resource = AUTH_RESOURCE language_server: Optional[Text] = None + async def pre_get(self): + """Handle a pre_get.""" + # authenticate first + # 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) + + if not hasattr(self, "authorizer"): + return + + # authorize the user. + is_authorized = await ensure_async( + self.authorizer.is_authorized(self, user, "execute", AUTH_RESOURCE) + ) + if not is_authorized: + raise web.HTTPError(403) + + async def get(self, *args, **kwargs): + """Get an event socket.""" + await self.pre_get() + res = super().get(*args, **kwargs) + if res is not None: + await res + async def open(self, language_server): await self.manager.ready() self.language_server = language_server @@ -47,11 +98,11 @@ Response should conform to schema in schema/servers.schema.json """ + auth_resource = AUTH_RESOURCE validator = SERVERS_RESPONSE - def initialize(self, *args, **kwargs): - super().initialize(*args, **kwargs) - + @web.authenticated + @authorized async def get(self): """finish with the JSON representations of the sessions""" await self.manager.ready() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/paths.py new/jupyter-lsp-2.2.2/jupyter_lsp/paths.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/paths.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/paths.py 2024-01-17 22:58:59.000000000 +0100 @@ -1,6 +1,7 @@ import os -import pathlib import re +from pathlib import Path +from typing import Union from urllib.parse import unquote, urlparse RE_PATH_ANCHOR = r"^file://([^/]+|/[A-Z]:)" @@ -12,7 +13,7 @@ Special care must be taken around windows paths: the canonical form of windows drives and UNC paths is lower case """ - root_uri = pathlib.Path(root_dir).expanduser().resolve().as_uri() + root_uri = Path(root_dir).expanduser().resolve().as_uri() root_uri = re.sub( RE_PATH_ANCHOR, lambda m: "file://{}".format(m.group(1).lower()), root_uri ) @@ -33,3 +34,12 @@ else: result = file_uri_path_unquoted # pragma: no cover return result + + +def is_relative(root: Union[str, Path], path: Union[str, Path]) -> bool: + """Return if path is relative to root""" + try: + Path(path).resolve().relative_to(Path(root).resolve()) + return True + except ValueError: + return False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/serverextension.py new/jupyter-lsp-2.2.2/jupyter_lsp/serverextension.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/serverextension.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/serverextension.py 2024-01-17 22:58:59.000000000 +0100 @@ -4,6 +4,7 @@ from pathlib import Path import traitlets +from tornado import ioloop from .handlers import add_handlers from .manager import LanguageServerManager @@ -73,4 +74,11 @@ page_config.update(rootUri=root_uri, virtualDocumentsUri=virtual_documents_uri) add_handlers(nbapp) - nbapp.io_loop.call_later(0, initialize, nbapp, virtual_documents_uri) + + if hasattr(nbapp, "io_loop"): + io_loop = nbapp.io_loop + else: + # handle jupyter_server 1.x + io_loop = ioloop.IOLoop.current() + + io_loop.call_later(0, initialize, nbapp, virtual_documents_uri) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/tests/conftest.py new/jupyter-lsp-2.2.2/jupyter_lsp/tests/conftest.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/tests/conftest.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/tests/conftest.py 2024-01-17 22:58:59.000000000 +0100 @@ -5,6 +5,7 @@ from jupyter_server.serverapp import ServerApp from pytest import fixture +from tornado.httpserver import HTTPRequest from tornado.httputil import HTTPServerRequest from tornado.queues import Queue from tornado.web import Application @@ -141,9 +142,11 @@ class MockHandler(LanguageServersHandler): _payload = None + _jupyter_current_user = "foo" # type:ignore[assignment] def __init__(self): - pass + self.request = HTTPRequest("GET") + self.application = Application() def finish(self, payload): self._payload = payload diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_auth.py new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_auth.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_auth.py 1970-01-01 01:00:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_auth.py 2024-01-17 22:58:59.000000000 +0100 @@ -0,0 +1,117 @@ +"""Integration tests of authorization running under jupyter-server.""" +import json +import os +import socket +import subprocess +import time +import uuid +from typing import Generator, Tuple +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + +import pytest + +from .conftest import KNOWN_SERVERS + +LOCALHOST = "127.0.0.1" +REST_ROUTES = ["/lsp/status"] +WS_ROUTES = [f"/lsp/ws/{ls}" for ls in KNOWN_SERVERS] + + +@pytest.mark.parametrize("route", REST_ROUTES) +def test_auth_rest(route: str, a_server_url_and_token: Tuple[str, str]) -> None: + """Verify a REST route only provides access to an authenticated user.""" + base_url, token = a_server_url_and_token + + verify_response(base_url, route) + + url = f"{base_url}{route}" + + with urlopen(f"{url}?token={token}") as response: + raw_body = response.read().decode("utf-8") + + decode_error = None + + try: + json.loads(raw_body) + except json.decoder.JSONDecodeError as err: + decode_error = err + assert not decode_error, f"the response for {url} was not JSON" + + +@pytest.mark.parametrize("route", WS_ROUTES) +def test_auth_websocket(route: str, a_server_url_and_token: Tuple[str, str]) -> None: + """Verify a WebSocket does not provide access to an unauthenticated user.""" + verify_response(a_server_url_and_token[0], route) + + +@pytest.fixture(scope="module") +def a_server_url_and_token( + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[Tuple[str, str], None, None]: + """Start a temporary, isolated jupyter server.""" + token = str(uuid.uuid4()) + port = get_unused_port() + + root_dir = tmp_path_factory.mktemp("root_dir") + home = tmp_path_factory.mktemp("home") + server_conf = home / "etc/jupyter/jupyter_config.json" + + server_conf.parent.mkdir(parents=True) + extensions = {"jupyter_lsp": True, "jupyterlab": False, "nbclassic": False} + app = {"jpserver_extensions": extensions, "token": token} + config_data = {"ServerApp": app, "IdentityProvider": {"token": token}} + + server_conf.write_text(json.dumps(config_data), encoding="utf-8") + args = ["jupyter-server", f"--port={port}", "--no-browser"] + env = dict(os.environ) + env.update( + HOME=str(home), + USERPROFILE=str(home), + JUPYTER_CONFIG_DIR=str(server_conf.parent), + ) + proc = subprocess.Popen(args, cwd=str(root_dir), env=env, stdin=subprocess.PIPE) + url = f"http://{LOCALHOST}:{port}" + retries = 20 + while retries: + time.sleep(1) + try: + urlopen(f"{url}/favicon.ico") + break + except URLError: + print(f"[{retries} / 20] ...", flush=True) + retries -= 1 + continue + yield url, token + proc.terminate() + proc.communicate(b"y\n") + proc.wait() + assert proc.returncode is not None, "jupyter-server probably still running" + + +def get_unused_port(): + """Get an unused port by trying to listen to any random port. + + Probably could introduce race conditions if inside a tight loop. + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((LOCALHOST, 0)) + sock.listen(1) + port = sock.getsockname()[1] + sock.close() + return port + + +def verify_response(base_url: str, route: str, expect: int = 403): + """Verify that a response returns the expected error.""" + error = None + body = None + url = f"{base_url}{route}" + try: + with urlopen(url) as res: + body = res.read() + except HTTPError as err: + error = err + assert error, f"no HTTP error for {url}: {body}" + http_code = error.getcode() + assert http_code == expect, f"{url} HTTP code was unexpected: {body}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_paths.py new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_paths.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_paths.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_paths.py 2024-01-17 22:58:59.000000000 +0100 @@ -4,7 +4,7 @@ import pytest -from ..paths import file_uri_to_path, normalized_uri +from ..paths import file_uri_to_path, is_relative, normalized_uri WIN = platform.system() == "Windows" HOME = pathlib.Path("~").expanduser() @@ -17,6 +17,45 @@ assert normalized_uri(root_dir) == expected_root_uri +@pytest.mark.skipif(WIN, reason="can't test POSIX paths on Windows") +@pytest.mark.parametrize( + "root, path", + [["~", "~/a"], ["~", "~/a/../b/"], ["/", "/"], ["/a", "/a/b"], ["/a", "/a/b/../c"]], +) +def test_is_relative_ok(root, path): + assert is_relative(root, path) + + +@pytest.mark.skipif(WIN, reason="can't test POSIX paths on Windows") +@pytest.mark.parametrize( + "root, path", + [ + ["~", "~/.."], + ["~", "/"], + ["/a", "/"], + ["/a/b", "/a"], + ["/a/b", "/a/b/.."], + ["/a", "/a/../b"], + ["/a", "a//"], + ], +) +def test_is_relative_not_ok(root, path): + assert not is_relative(root, path) + + +@pytest.mark.skipif(not WIN, reason="can't test Windows paths on POSIX") +@pytest.mark.parametrize( + "root, path", + [ + ["c:\\Users\\user1", "c:\\Users\\"], + ["c:\\Users\\user1", "d:\\"], + ["c:\\Users", "c:\\Users\\.."], + ], +) +def test_is_relative_not_ok_win(root, path): + assert not is_relative(root, path) + + @pytest.mark.skipif(PY35, reason="can't test non-existent paths on py35") @pytest.mark.skipif(WIN, reason="can't test POSIX paths on Windows") @pytest.mark.parametrize( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_virtual_documents_shadow.py new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_virtual_documents_shadow.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/tests/test_virtual_documents_shadow.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/tests/test_virtual_documents_shadow.py 2024-01-17 22:58:59.000000000 +0100 @@ -210,6 +210,21 @@ ) +@pytest.mark.asyncio +async def test_shadow_traversal(shadow_path, manager): + file_beyond_shadow_root_uri = (Path(shadow_path) / ".." / "test.py").as_uri() + + shadow = setup_shadow_filesystem(Path(shadow_path).as_uri()) + + def run_shadow(message): + return shadow("client", message, "python-lsp-server", manager) + + with pytest.raises( + ShadowFilesystemError, match="is not relative to shadow filesystem root" + ): + await run_shadow(did_open(file_beyond_shadow_root_uri, "content")) + + @pytest.fixture def forbidden_shadow_path(tmpdir): path = Path(tmpdir) / "no_permission_dir" @@ -238,7 +253,7 @@ # no message should be emitted during the first two attempts assert caplog.text == "" - # a wargning should be emitted on third failure + # a warning should be emitted on third failure with caplog.at_level(logging.WARNING): assert await send_change() is None assert "initialization of shadow filesystem failed three times" in caplog.text diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp/virtual_documents_shadow.py new/jupyter-lsp-2.2.2/jupyter_lsp/virtual_documents_shadow.py --- old/jupyter-lsp-2.2.1/jupyter_lsp/virtual_documents_shadow.py 2023-11-26 21:27:00.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp/virtual_documents_shadow.py 2024-01-17 22:58:59.000000000 +0100 @@ -8,7 +8,7 @@ from tornado.gen import convert_yielded from .manager import lsp_message_listener -from .paths import file_uri_to_path +from .paths import file_uri_to_path, is_relative from .types import LanguageServerManagerAPI # TODO: make configurable @@ -171,6 +171,11 @@ initialized = True path = file_uri_to_path(uri) + if not is_relative(shadow_filesystem, path): + raise ShadowFilesystemError( + f"Path {path} is not relative to shadow filesystem root" + ) + editable_file = EditableFile(path) await editable_file.read() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp.egg-info/PKG-INFO new/jupyter-lsp-2.2.2/jupyter_lsp.egg-info/PKG-INFO --- old/jupyter-lsp-2.2.1/jupyter_lsp.egg-info/PKG-INFO 2023-11-26 21:29:20.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp.egg-info/PKG-INFO 2024-01-17 23:02:01.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: jupyter-lsp -Version: 2.2.1 +Version: 2.2.2 Summary: Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server Author: jupyter-lsp Contributors Author-email: project.jupy...@gmail.com diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jupyter-lsp-2.2.1/jupyter_lsp.egg-info/SOURCES.txt new/jupyter-lsp-2.2.2/jupyter_lsp.egg-info/SOURCES.txt --- old/jupyter-lsp-2.2.1/jupyter_lsp.egg-info/SOURCES.txt 2023-11-26 21:29:20.000000000 +0100 +++ new/jupyter-lsp-2.2.2/jupyter_lsp.egg-info/SOURCES.txt 2024-01-17 23:02:02.000000000 +0100 @@ -62,6 +62,7 @@ jupyter_lsp/tests/__init__.py jupyter_lsp/tests/conftest.py jupyter_lsp/tests/listener.py +jupyter_lsp/tests/test_auth.py jupyter_lsp/tests/test_bad_spec.py jupyter_lsp/tests/test_conf_d.py jupyter_lsp/tests/test_detect.py