Files

177 lines
5.9 KiB
Python

"""The filesystem allowlist: which server-side paths a source may ever read.
Rejecting traversal, symlinks and relative paths only proves a path is well
formed. These tests are about the separate question the allowlist answers --
whether a well-formed path is one an operator ever intended RAGcore to read.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from ragcore.domain.sources import (
SourceRootError,
assert_within_allowed_roots,
normalize_allowed_roots,
resolve_within_allowed_roots,
)
pytestmark = pytest.mark.security
_ROOTS = (Path("/mnt/rag-sources"), Path("/srv/corpora"))
def test_no_configured_roots_allows_nothing() -> None:
"""Fail closed. An empty allowlist is a real answer -- "this deployment
reads no server-side paths" -- not a missing one to be defaulted away."""
with pytest.raises(SourceRootError, match="no allowed source roots"):
assert_within_allowed_roots("/mnt/rag-sources/runbooks", ())
@pytest.mark.parametrize(
"requested",
[
"/mnt/rag-sources",
"/mnt/rag-sources/runbooks",
"/mnt/rag-sources/runbooks/fleet",
"/srv/corpora/handbook",
],
)
def test_a_root_and_anything_below_it_is_allowed(requested: str) -> None:
assert assert_within_allowed_roots(requested, _ROOTS) == Path(requested)
@pytest.mark.parametrize(
"requested",
[
"/etc",
"/etc/shadow",
"/app/src/ragcore/config.py",
"/var/lib/postgresql/data",
"/mnt",
"/",
],
)
def test_a_path_outside_every_root_is_refused(requested: str) -> None:
with pytest.raises(SourceRootError, match="outside every allowed source root"):
assert_within_allowed_roots(requested, _ROOTS)
@pytest.mark.parametrize(
"requested",
[
"/mnt/rag-sources-secret",
"/mnt/rag-sources-secret/keys",
"/srv/corpora-backup",
],
)
def test_a_sibling_sharing_a_string_prefix_is_not_contained(requested: str) -> None:
"""Containment is decided on path *components*, never on string prefixes.
``/mnt/rag-sources`` does not contain ``/mnt/rag-sources-secret``, however
much the two look alike to ``startswith``."""
with pytest.raises(SourceRootError, match="outside every allowed source root"):
assert_within_allowed_roots(requested, _ROOTS)
@pytest.mark.parametrize(
"requested",
["relative/path", "", "rag-sources", "C:x"],
)
def test_a_path_that_is_not_absolute_is_refused(requested: str) -> None:
with pytest.raises(SourceRootError):
assert_within_allowed_roots(requested, _ROOTS)
def test_traversal_is_refused_before_containment_is_even_considered() -> None:
"""``/mnt/rag-sources/../../etc`` normalises to ``/etc``, so a check that
normalised first and compared second would be fine either way -- but the
path is rejected outright, so a root that happens to be near the top of
the tree cannot be walked out of."""
with pytest.raises(SourceRootError, match="must not traverse"):
assert_within_allowed_roots("/mnt/rag-sources/../../etc", _ROOTS)
def test_a_nul_byte_is_refused() -> None:
with pytest.raises(SourceRootError, match="NUL-free"):
assert_within_allowed_roots("/mnt/rag-sources/x\x00.txt", _ROOTS)
def test_the_stored_spelling_survives_validation() -> None:
"""The API host validating a POSIX deployment path must hand back exactly
what the operator wrote: a Windows-side ``normpath`` would rewrite it into
something the Linux worker could never open."""
assert str(assert_within_allowed_roots("/mnt/rag-sources/docs", _ROOTS)).replace(
"\\", "/"
) == "/mnt/rag-sources/docs"
# -- operator-configured roots ----------------------------------------------
def test_a_filesystem_root_may_not_be_an_allowed_root() -> None:
"""Allowing ``/`` would make the allowlist meaningless."""
with pytest.raises(SourceRootError, match="must not be a filesystem root"):
normalize_allowed_roots(["/"])
def test_a_relative_or_traversing_root_is_refused_at_startup() -> None:
with pytest.raises(SourceRootError, match="absolute"):
normalize_allowed_roots(["mnt/rag-sources"])
with pytest.raises(SourceRootError, match="traverse"):
normalize_allowed_roots(["/mnt/rag-sources/../.."])
def test_duplicate_roots_collapse() -> None:
assert normalize_allowed_roots(["/mnt/rag", "/mnt/rag"]) == (Path("/mnt/rag"),)
# -- filesystem truth --------------------------------------------------------
def test_resolution_rejects_a_path_that_does_not_exist(tmp_path: Path) -> None:
"""The worker's check is about the filesystem as it is: a lexically fine
path that cannot be resolved is not a path it may read."""
with pytest.raises(SourceRootError, match="cannot be resolved"):
resolve_within_allowed_roots(tmp_path / "absent", (tmp_path,))
def test_resolution_accepts_a_real_directory_inside_an_allowed_root(tmp_path: Path) -> None:
allowed = tmp_path / "corpora"
inside = allowed / "runbooks"
inside.mkdir(parents=True)
assert resolve_within_allowed_roots(inside, (allowed,)) == inside.resolve(strict=True)
def test_resolution_refuses_a_real_directory_outside_every_allowed_root(tmp_path: Path) -> None:
allowed = tmp_path / "corpora"
allowed.mkdir()
elsewhere = tmp_path / "secrets"
elsewhere.mkdir()
with pytest.raises(SourceRootError, match="outside every allowed source root"):
resolve_within_allowed_roots(elsewhere, (allowed,))
def test_an_unmounted_root_contains_nothing_but_does_not_break_the_others(
tmp_path: Path,
) -> None:
"""A deployment may configure roots that only some containers mount. One
that is absent here simply cannot contain anything; the roots that are
present still get their chance."""
allowed = tmp_path / "corpora"
allowed.mkdir()
assert resolve_within_allowed_roots(
allowed, (tmp_path / "never-mounted", allowed)
) == allowed.resolve(strict=True)