|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import os |
| 4 | +import subprocess |
| 5 | + |
| 6 | +from datacustomcode.template import script_template_dir |
| 7 | + |
| 8 | +JUPYTERLAB_SH = os.path.join(script_template_dir, "jupyterlab.sh") |
| 9 | + |
| 10 | +# These tests don't actually run the jupyter script. They simply verify |
| 11 | +# certain specific configurations of the script for things like syntax |
| 12 | +# and security correctness. |
| 13 | +# |
| 14 | +# These were added when fixing a bug that could have allowed for RCE |
| 15 | +# over the local network on the user's device due to previous insufficient |
| 16 | +# network config. While not perfect, they do offer a bit of assurance that |
| 17 | +# the script is configured correctly. |
| 18 | + |
| 19 | + |
| 20 | +class TestJupyterlabScript: |
| 21 | + def _read(self) -> str: |
| 22 | + with open(JUPYTERLAB_SH) as f: |
| 23 | + return f.read() |
| 24 | + |
| 25 | + def test_jupyterlab_sh_syntax_is_valid(self): |
| 26 | + """`bash -n` should accept the script without syntax errors.""" |
| 27 | + result = subprocess.run( |
| 28 | + ["bash", "-n", JUPYTERLAB_SH], |
| 29 | + capture_output=True, |
| 30 | + text=True, |
| 31 | + check=False, |
| 32 | + ) |
| 33 | + assert result.returncode == 0, result.stderr |
| 34 | + |
| 35 | + def test_start_jupyter_binds_loopback_host_port(self): |
| 36 | + content = self._read() |
| 37 | + assert "-p 127.0.0.1:8888:8888" in content |
| 38 | + assert "-p 8888:8888" not in content |
| 39 | + |
| 40 | + def test_start_jupyter_binds_container_to_all_interfaces(self): |
| 41 | + content = self._read() |
| 42 | + assert "--ip=0.0.0.0" in content |
| 43 | + assert "--ip=127.0.0.1" not in content |
| 44 | + |
| 45 | + def test_start_jupyter_generates_token_not_empty_auth(self): |
| 46 | + content = self._read() |
| 47 | + assert "--NotebookApp.token=''" not in content |
| 48 | + assert "--NotebookApp.password=''" not in content |
| 49 | + assert "openssl rand -hex 32" in content |
| 50 | + |
| 51 | + def test_start_jupyter_uses_dynamic_token_variable(self): |
| 52 | + content = self._read() |
| 53 | + assert "local TOKEN" in content |
| 54 | + assert "TOKEN=$(openssl rand -hex 32)" in content |
| 55 | + assert '--NotebookApp.token="$TOKEN"' in content |
| 56 | + |
| 57 | + def test_open_browser_url_includes_token_param(self): |
| 58 | + content = self._read() |
| 59 | + assert 'URL="http://localhost:8888/?token=$TOKEN"' in content |
| 60 | + assert "open_browser $URL" in content |
| 61 | + |
| 62 | + def test_token_never_written_to_file(self): |
| 63 | + content = self._read() |
| 64 | + assert "credentials.ini" not in content |
| 65 | + for line in content.splitlines(): |
| 66 | + if "TOKEN" in line: |
| 67 | + assert ">" not in line, f"Line writes TOKEN to a file: {line!r}" |
0 commit comments