177 lines
5.9 KiB
Python
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)
|