mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
* feat(webui): add project workspaces and access controls * feat(webui): add project workspaces and access controls * refactor(tools): centralize workspace access resolution * refactor(webui): remove unused workspace host state * fix(webui): hide estimated file edit label * fix(webui): clarify file edit deletion feedback * fix(webui): label deleted file activity * fix(webui): flatten file edit activity rows * fix(core): remove path-only patch deletion * fix(core): keep apply patch non-destructive * refactor(webui): trim workspace host plumbing * fix(tools): register exec with tools config
70 lines
2.1 KiB
Python
70 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nanobot.security.workspace_policy import (
|
|
WorkspaceBoundaryError,
|
|
is_path_within,
|
|
resolve_allowed_path,
|
|
)
|
|
|
|
|
|
def test_resolve_allowed_path_accepts_workspace_relative_path(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
target = workspace / "src" / "main.py"
|
|
target.parent.mkdir()
|
|
target.write_text("print('ok')", encoding="utf-8")
|
|
|
|
resolved = resolve_allowed_path("src/main.py", workspace=workspace, allowed_root=workspace)
|
|
|
|
assert resolved == target.resolve()
|
|
|
|
|
|
def test_resolve_allowed_path_blocks_parent_traversal(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
outside = tmp_path / "secret.txt"
|
|
outside.write_text("secret", encoding="utf-8")
|
|
|
|
with pytest.raises(WorkspaceBoundaryError, match="outside allowed directory"):
|
|
resolve_allowed_path("../secret.txt", workspace=workspace, allowed_root=workspace)
|
|
|
|
|
|
def test_resolve_allowed_path_blocks_symlink_escape(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
outside = tmp_path / "outside"
|
|
outside.mkdir()
|
|
secret = outside / "secret.txt"
|
|
secret.write_text("secret", encoding="utf-8")
|
|
link = workspace / "linked-secret.txt"
|
|
try:
|
|
link.symlink_to(secret)
|
|
except OSError as exc:
|
|
pytest.skip(f"symlink creation is unavailable: {exc}")
|
|
|
|
assert not is_path_within(link, workspace)
|
|
with pytest.raises(WorkspaceBoundaryError):
|
|
resolve_allowed_path("linked-secret.txt", workspace=workspace, allowed_root=workspace)
|
|
|
|
|
|
def test_resolve_allowed_path_allows_extra_root(tmp_path: Path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
media = tmp_path / "media"
|
|
media.mkdir()
|
|
image = media / "image.png"
|
|
image.write_bytes(b"\x89PNG\r\n\x1a\n")
|
|
|
|
resolved = resolve_allowed_path(
|
|
image,
|
|
workspace=workspace,
|
|
allowed_root=workspace,
|
|
extra_allowed_roots=[media],
|
|
)
|
|
|
|
assert resolved == image.resolve()
|