Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
22 changes: 18 additions & 4 deletions src/datacustomcode/templates/script/jupyterlab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,39 @@ 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 \
--ip=0.0.0.0 \
--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
Expand All @@ -82,6 +95,7 @@ stop_jupyter() {
case "$1" in
"start")
check_docker
check_openssl
start_jupyter
;;
"stop")
Expand Down
67 changes: 67 additions & 0 deletions tests/test_jupyterlab_script.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading