From 6f84341efbf1d6d068c3991e7487fd5acaaaa0a4 Mon Sep 17 00:00:00 2001 From: Zach Maddox Date: Wed, 12 Aug 2026 19:27:10 -0400 Subject: [PATCH] jupyter bind loopback, use token --- README.md | 4 +- .../templates/script/jupyterlab.sh | 22 ++++-- tests/test_jupyterlab_script.py | 67 +++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/test_jupyterlab_script.py diff --git a/README.md b/README.md index 3cf73b9..3452d04 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ exploration. Instead of running an entire script, one can run one code cell at You can read more about Jupyter Notebooks here: https://jupyter.org/ -1. Within the root project of your package folder, run `./jupyterlab.sh start` +1. Within the root project of your package folder, run `./jupyterlab.sh start`. This prints an access token and opens an already-authenticated JupyterLab session in your browser. If the browser doesn't open automatically, copy the printed `http://localhost:8888/?token=...` URL into your browser. 1. Double-click on "account.ipynb" file, which provides a starting point for a notebook 1. Use shift+enter to execute each cell within the notebook. Add/edit/delete cells of code as needed for your data exploration. 1. Don't forget to run `./jupyterlab.sh stop` to stop the docker container @@ -563,4 +563,4 @@ If you're using OAuth Tokens authentication, the initial configure will retrieve ## Other docs - [Troubleshooting](./docs/troubleshooting.md) -- [For Contributors](./FOR_CONTRIBUTORS.md) +- [Contributing](./CONTRIBUTING.md) diff --git a/src/datacustomcode/templates/script/jupyterlab.sh b/src/datacustomcode/templates/script/jupyterlab.sh index e8445fc..55829d0 100755 --- a/src/datacustomcode/templates/script/jupyterlab.sh +++ b/src/datacustomcode/templates/script/jupyterlab.sh @@ -45,13 +45,24 @@ check_docker() { echo "Docker daemon is running" } +# Function to check if openssl is installed +check_openssl() { + if ! command -v openssl &> /dev/null; then + echo "openssl is not installed. It is required to generate a secure JupyterLab access token." + exit 1 + fi +} + # Function to start Jupyter server start_jupyter() { echo "Building the docker image" docker build -t datacloud-customcode . + local TOKEN + TOKEN=$(openssl rand -hex 32) + echo "Running the docker container" - docker run -d --rm -p 8888:8888 \ + docker run -d --rm -p 127.0.0.1:8888:8888 \ -v $(pwd):/workspace \ --name jupyter-server \ datacloud-customcode jupyter lab \ @@ -59,12 +70,14 @@ start_jupyter() { --port=8888 \ --no-browser \ --allow-root \ - --NotebookApp.token='' \ - --NotebookApp.password='' \ + --NotebookApp.token="$TOKEN" \ --notebook-dir=/workspace sleep 3 # Wait for server to start - open_browser "http://localhost:8888" + local URL + URL="http://localhost:8888/?token=$TOKEN" + echo "Opening $URL" + open_browser $URL } # Function to stop Jupyter server @@ -82,6 +95,7 @@ stop_jupyter() { case "$1" in "start") check_docker + check_openssl start_jupyter ;; "stop") diff --git a/tests/test_jupyterlab_script.py b/tests/test_jupyterlab_script.py new file mode 100644 index 0000000..082f583 --- /dev/null +++ b/tests/test_jupyterlab_script.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import os +import subprocess + +from datacustomcode.template import script_template_dir + +JUPYTERLAB_SH = os.path.join(script_template_dir, "jupyterlab.sh") + +# These tests don't actually run the jupyter script. They simply verify +# certain specific configurations of the script for things like syntax +# and security correctness. +# +# These were added when fixing a bug that could have allowed for RCE +# over the local network on the user's device due to previous insufficient +# network config. While not perfect, they do offer a bit of assurance that +# the script is configured correctly. + + +class TestJupyterlabScript: + def _read(self) -> str: + with open(JUPYTERLAB_SH) as f: + return f.read() + + def test_jupyterlab_sh_syntax_is_valid(self): + """`bash -n` should accept the script without syntax errors.""" + result = subprocess.run( + ["bash", "-n", JUPYTERLAB_SH], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + def test_start_jupyter_binds_loopback_host_port(self): + content = self._read() + assert "-p 127.0.0.1:8888:8888" in content + assert "-p 8888:8888" not in content + + def test_start_jupyter_binds_container_to_all_interfaces(self): + content = self._read() + assert "--ip=0.0.0.0" in content + assert "--ip=127.0.0.1" not in content + + def test_start_jupyter_generates_token_not_empty_auth(self): + content = self._read() + assert "--NotebookApp.token=''" not in content + assert "--NotebookApp.password=''" not in content + assert "openssl rand -hex 32" in content + + def test_start_jupyter_uses_dynamic_token_variable(self): + content = self._read() + assert "local TOKEN" in content + assert "TOKEN=$(openssl rand -hex 32)" in content + assert '--NotebookApp.token="$TOKEN"' in content + + def test_open_browser_url_includes_token_param(self): + content = self._read() + assert 'URL="http://localhost:8888/?token=$TOKEN"' in content + assert "open_browser $URL" in content + + def test_token_never_written_to_file(self): + content = self._read() + assert "credentials.ini" not in content + for line in content.splitlines(): + if "TOKEN" in line: + assert ">" not in line, f"Line writes TOKEN to a file: {line!r}"