Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"""Adversarial tests for the read-only folder connector."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.domain.sources import FolderSourceConfig, SourceCursor, SourceValidationError
|
||||
from ragcore.infrastructure.connectors.folder import (
|
||||
FolderConnector,
|
||||
FolderTraversalError,
|
||||
)
|
||||
|
||||
|
||||
def connector(
|
||||
root: Path, *, max_items: int = 256, allowed_roots: tuple[Path, ...] | None = None
|
||||
) -> FolderConnector:
|
||||
"""Traversal tests configure the root under test as its own allowed root:
|
||||
they are about escaping *this* root, and the allowlist is exercised on its
|
||||
own below."""
|
||||
|
||||
return FolderConnector(
|
||||
uuid4(),
|
||||
FolderSourceConfig(root),
|
||||
allowed_roots=(root,) if allowed_roots is None else allowed_roots,
|
||||
max_items=max_items,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_scan_is_deterministic_cursorable_and_partial_scans_cannot_delete() -> None:
|
||||
root = (Path(".") / "folder-test-root").resolve()
|
||||
root.mkdir()
|
||||
try:
|
||||
(root / "a.txt").write_text("alpha", encoding="utf-8")
|
||||
(root / "nested").mkdir()
|
||||
(root / "nested" / "b.md").write_text("bravo", encoding="utf-8")
|
||||
current = connector(root, max_items=1)
|
||||
|
||||
first = await current.scan()
|
||||
assert first.complete is False
|
||||
assert first.next_cursor is not None
|
||||
assert first.items[0].external_id == "a.txt"
|
||||
assert first.deleted_item_ids({"a.txt", "nested/b.md", "deleted.txt"}) == frozenset()
|
||||
|
||||
second = await current.scan(first.next_cursor)
|
||||
assert second.complete is False
|
||||
assert second.next_cursor is None
|
||||
assert [item.external_id for item in second.items] == ["nested/b.md"]
|
||||
assert second.deleted_item_ids({"a.txt", "nested/b.md", "deleted.txt"}) == frozenset()
|
||||
finally:
|
||||
(root / "nested" / "b.md").unlink(missing_ok=True)
|
||||
(root / "nested").rmdir()
|
||||
(root / "a.txt").unlink(missing_ok=True)
|
||||
root.rmdir()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_symlink_and_traversal_are_denied() -> None:
|
||||
root = (Path(".") / "folder-symlink-root").resolve()
|
||||
outside = (Path(".") / "folder-symlink-outside.txt").resolve()
|
||||
root.mkdir()
|
||||
outside.write_text("outside", encoding="utf-8")
|
||||
link = root / "escape.txt"
|
||||
try:
|
||||
link.symlink_to(outside)
|
||||
current = connector(root)
|
||||
with pytest.raises(FolderTraversalError):
|
||||
await current.scan()
|
||||
with pytest.raises(FolderTraversalError):
|
||||
await current.scan(SourceCursor("../escape.txt"))
|
||||
finally:
|
||||
link.unlink(missing_ok=True)
|
||||
outside.unlink(missing_ok=True)
|
||||
root.rmdir()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_acquisition_is_read_only_and_rejects_cross_root_item() -> None:
|
||||
root = (Path(".") / "folder-readonly-root").resolve()
|
||||
root.mkdir()
|
||||
original = b"read only source"
|
||||
source = root / "source.txt"
|
||||
source.write_bytes(original)
|
||||
try:
|
||||
current = connector(root)
|
||||
result = await current.scan()
|
||||
acquisition = await current.acquire(result.items[0])
|
||||
assert b"".join([chunk async for chunk in acquisition.stream]) == original
|
||||
assert source.read_bytes() == original
|
||||
assert list(root.iterdir()) == [source]
|
||||
|
||||
with pytest.raises(FolderTraversalError):
|
||||
await current.acquire(
|
||||
result.items[0].__class__(
|
||||
source_id=result.items[0].source_id,
|
||||
external_id="../escape.txt",
|
||||
locator="../escape.txt",
|
||||
name="escape.txt",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
source.unlink(missing_ok=True)
|
||||
root.rmdir()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_unchanged_items_skip_acquisition_by_fingerprint() -> None:
|
||||
root = (Path(".") / "folder-fingerprint-root").resolve()
|
||||
root.mkdir()
|
||||
source = root / "source.txt"
|
||||
source.write_text("first", encoding="utf-8")
|
||||
try:
|
||||
current = connector(root)
|
||||
first = (await current.scan()).items[0]
|
||||
fingerprint = current.fingerprint(first)
|
||||
assert current.should_acquire(first, fingerprint) is False
|
||||
|
||||
source.write_text("second", encoding="utf-8")
|
||||
changed = (await current.scan()).items[0]
|
||||
assert current.should_acquire(changed, fingerprint) is True
|
||||
finally:
|
||||
source.unlink(missing_ok=True)
|
||||
root.rmdir()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
def test_folder_config_rejects_symlink_root() -> None:
|
||||
root = (Path(".") / "folder-config-root").resolve()
|
||||
target = (Path(".") / "folder-config-target").resolve()
|
||||
target.mkdir()
|
||||
try:
|
||||
root.symlink_to(target, target_is_directory=True)
|
||||
with pytest.raises(SourceValidationError):
|
||||
FolderSourceConfig(root)
|
||||
finally:
|
||||
root.unlink(missing_ok=True)
|
||||
target.rmdir()
|
||||
|
||||
|
||||
# -- the allowlist, enforced by the connector itself -------------------------
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_a_root_outside_every_allowed_root_cannot_be_scanned(tmp_path: Path) -> None:
|
||||
"""A perfectly well-formed root -- absolute, no traversal, no symlink, a
|
||||
real directory -- is still refused when no operator ever allowed it."""
|
||||
|
||||
allowed = tmp_path / "corpora"
|
||||
allowed.mkdir()
|
||||
elsewhere = tmp_path / "secrets"
|
||||
elsewhere.mkdir()
|
||||
(elsewhere / "keys.txt").write_text("shhh", encoding="utf-8")
|
||||
|
||||
current = connector(elsewhere, allowed_roots=(allowed,))
|
||||
|
||||
with pytest.raises(FolderTraversalError, match="not an allowed source root"):
|
||||
await current.scan()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_no_configured_roots_means_no_folder_can_be_scanned(tmp_path: Path) -> None:
|
||||
root = tmp_path / "corpora"
|
||||
root.mkdir()
|
||||
(root / "a.txt").write_text("alpha", encoding="utf-8")
|
||||
|
||||
current = connector(root, allowed_roots=())
|
||||
|
||||
with pytest.raises(FolderTraversalError, match="not an allowed source root"):
|
||||
await current.scan()
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_a_directory_below_an_allowed_root_is_readable(tmp_path: Path) -> None:
|
||||
allowed = tmp_path / "corpora"
|
||||
root = allowed / "runbooks"
|
||||
root.mkdir(parents=True)
|
||||
(root / "a.txt").write_text("alpha", encoding="utf-8")
|
||||
|
||||
result = await connector(root, allowed_roots=(allowed,)).scan()
|
||||
|
||||
assert [item.external_id for item in result.items] == ["a.txt"]
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.anyio
|
||||
async def test_the_allowlist_is_re_checked_on_acquire_not_only_on_scan(tmp_path: Path) -> None:
|
||||
"""Every read revalidates. A connector that passed the check once must not
|
||||
keep reading if the answer would be different now."""
|
||||
|
||||
root = tmp_path / "corpora"
|
||||
root.mkdir()
|
||||
(root / "a.txt").write_text("alpha", encoding="utf-8")
|
||||
scanned = await connector(root, allowed_roots=(root,)).scan()
|
||||
[item] = scanned.items
|
||||
|
||||
disallowed = connector(root, allowed_roots=(tmp_path / "somewhere-else",))
|
||||
|
||||
with pytest.raises(FolderTraversalError, match="not an allowed source root"):
|
||||
await disallowed.acquire(item)
|
||||
Reference in new issue
Block a user