Squash-merges PR #610. Fixes Docker workspace env var override and trust validation (issue #609). 1367 tests passing, QA harness green. Reviewed by independent agent (see PR comments).
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""
|
|
Tests for GitHub issue #609 — Docker workspace path trust and env-var priority.
|
|
|
|
Two independent bugs were fixed:
|
|
|
|
1. HERMES_WEBUI_DEFAULT_WORKSPACE env var was silently overridden by
|
|
settings.json at server startup. The env var must always win.
|
|
|
|
2. resolve_trusted_workspace() rejected paths that are children of
|
|
DEFAULT_WORKSPACE (e.g. /data/workspace/project) when the default is a
|
|
Docker volume mount outside the user's home directory. Any path under
|
|
the boot-time default should be trusted automatically.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from api.workspace import resolve_trusted_workspace
|
|
|
|
|
|
# ── Fix 2: trust paths under DEFAULT_WORKSPACE ───────────────────────────────
|
|
|
|
def test_subdir_of_boot_default_is_trusted(monkeypatch, tmp_path):
|
|
"""A subdirectory of BOOT_DEFAULT_WORKSPACE must be trusted without being in
|
|
the saved workspace list and without being under the user's home directory.
|
|
|
|
This is the core Docker case: DEFAULT_WORKSPACE=/data/workspace, and the
|
|
user tries to open /data/workspace/myproject — should NOT raise ValueError.
|
|
"""
|
|
import api.workspace as ws_mod
|
|
|
|
boot_default = tmp_path / "data" / "workspace"
|
|
boot_default.mkdir(parents=True)
|
|
sub = boot_default / "myproject"
|
|
sub.mkdir()
|
|
|
|
monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default))
|
|
|
|
# Should not raise — sub is under the boot default
|
|
result = resolve_trusted_workspace(str(sub))
|
|
assert result == sub.resolve()
|
|
|
|
|
|
def test_boot_default_itself_is_trusted(monkeypatch, tmp_path):
|
|
"""The DEFAULT_WORKSPACE path itself must also be trusted (not only subdirs)."""
|
|
import api.workspace as ws_mod
|
|
|
|
boot_default = tmp_path / "data" / "workspace"
|
|
boot_default.mkdir(parents=True)
|
|
|
|
monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default))
|
|
|
|
result = resolve_trusted_workspace(str(boot_default))
|
|
assert result == boot_default.resolve()
|
|
|
|
|
|
def test_path_outside_boot_default_and_home_is_rejected(monkeypatch, tmp_path):
|
|
"""A path that is not under home, not in the saved list, and not under
|
|
DEFAULT_WORKSPACE must still be rejected."""
|
|
import api.workspace as ws_mod
|
|
|
|
boot_default = tmp_path / "data" / "workspace"
|
|
boot_default.mkdir(parents=True)
|
|
outside = tmp_path / "other_mount" / "secret"
|
|
outside.mkdir(parents=True)
|
|
|
|
monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default))
|
|
|
|
with pytest.raises(ValueError, match="outside the user home"):
|
|
resolve_trusted_workspace(str(outside))
|
|
|
|
|
|
def test_none_path_returns_boot_default(monkeypatch, tmp_path):
|
|
"""resolve_trusted_workspace(None) always returns the boot default unchanged."""
|
|
import api.workspace as ws_mod
|
|
|
|
boot_default = tmp_path / "data" / "workspace"
|
|
boot_default.mkdir(parents=True)
|
|
|
|
monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default))
|
|
|
|
result = resolve_trusted_workspace(None)
|
|
assert result == boot_default.resolve()
|
|
|
|
|
|
def test_path_traversal_via_dotdot_does_not_escape_boot_default(monkeypatch, tmp_path):
|
|
"""A path that uses `..` to escape DEFAULT_WORKSPACE must not be trusted by (C).
|
|
|
|
`Path.resolve()` collapses `..` before the `relative_to(boot_default)` check
|
|
runs, so `/data/workspace/../etc` resolves to `/etc` and is rejected (it's
|
|
also caught earlier by the system-roots block, but this test pins the
|
|
behavior in case the order of conditions ever changes).
|
|
"""
|
|
import api.workspace as ws_mod
|
|
|
|
boot_default = tmp_path / "data" / "workspace"
|
|
boot_default.mkdir(parents=True)
|
|
sibling = tmp_path / "data" / "private"
|
|
sibling.mkdir(parents=True)
|
|
|
|
monkeypatch.setattr(ws_mod, "_BOOT_DEFAULT_WORKSPACE", str(boot_default))
|
|
|
|
# `boot_default/../private` resolves to `tmp_path/data/private`, which is
|
|
# NOT a child of boot_default and not under home — must reject.
|
|
escape = boot_default / ".." / "private"
|
|
with pytest.raises(ValueError, match="outside the user home"):
|
|
resolve_trusted_workspace(str(escape))
|