fix(v2): enforce repository safety failures
This commit is contained in:
@@ -29,10 +29,19 @@ def blob_digest(content: bytes) -> str:
|
||||
|
||||
|
||||
def _contained(root: Path, relative_path: str) -> Path:
|
||||
candidate = (root / relative_path).resolve()
|
||||
if not relative_path or Path(relative_path).is_absolute() or candidate == root:
|
||||
relative = Path(relative_path)
|
||||
if not relative_path or relative.is_absolute():
|
||||
raise RepositoryError("repository path must be a non-empty relative child")
|
||||
if root not in candidate.parents:
|
||||
lexical = root
|
||||
for part in relative.parts:
|
||||
lexical /= part
|
||||
if lexical.is_symlink():
|
||||
raise RepositoryError("repository path contains a symlink")
|
||||
candidate = lexical.resolve()
|
||||
resolved_root = root.resolve()
|
||||
if candidate == resolved_root:
|
||||
raise RepositoryError("repository path must be a non-empty relative child")
|
||||
if resolved_root not in candidate.parents:
|
||||
raise RepositoryError("repository path escapes configured roots")
|
||||
return candidate
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -30,7 +30,7 @@ def settings_for(tmp_path: Path, **overrides: object) -> Settings:
|
||||
"min_free_bytes": 1,
|
||||
}
|
||||
values.update(overrides)
|
||||
return Settings(**values)
|
||||
return Settings.model_validate(values)
|
||||
|
||||
|
||||
def test_partial_initialization_is_removed_on_publish_failure(tmp_path: Path) -> None:
|
||||
@@ -79,6 +79,26 @@ def test_initialization_rejects_insufficient_capacity(
|
||||
initialize(settings, "main", "none", "none")
|
||||
|
||||
|
||||
def test_initialization_emits_complete_repository_protocol_metadata(tmp_path: Path) -> None:
|
||||
result = initialize(settings_for(tmp_path), "main", "none", "none")
|
||||
try:
|
||||
payload = json.loads((result.root / "repository.json").read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise AssertionError("repository metadata was not readable JSON") from error
|
||||
assert set(payload) == {
|
||||
"repository_id",
|
||||
"format_version",
|
||||
"digest_algorithm",
|
||||
"compression",
|
||||
"encryption",
|
||||
"created_at",
|
||||
}
|
||||
assert payload["repository_id"][14] == "7"
|
||||
assert payload["digest_algorithm"] == "sha256"
|
||||
assert payload["encryption"] == {"mode": "none", "key_id": None}
|
||||
assert payload["created_at"].endswith("Z")
|
||||
|
||||
|
||||
def test_repository_inspection_rejects_noncanonical_metadata(tmp_path: Path) -> None:
|
||||
settings = settings_for(tmp_path)
|
||||
result = initialize(settings, "main", "none", "none")
|
||||
@@ -120,21 +140,29 @@ async def test_database_failure_removes_published_repository(tmp_path: Path) ->
|
||||
await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD})
|
||||
).status_code == 201
|
||||
csrf = client.cookies["backup_tool_csrf"]
|
||||
with patch(
|
||||
"backup_tool.api.app.audit", new=AsyncMock(side_effect=RuntimeError("db failure"))
|
||||
):
|
||||
response = await client.post(
|
||||
"/api/v2/repositories",
|
||||
json={
|
||||
"name": "main",
|
||||
"relative_path": "main",
|
||||
"compression": "none",
|
||||
"encryption": "none",
|
||||
},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
first = await client.post(
|
||||
"/api/v2/repositories",
|
||||
json={
|
||||
"name": "main",
|
||||
"relative_path": "first",
|
||||
"compression": "none",
|
||||
"encryption": "none",
|
||||
},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
assert first.status_code == 201
|
||||
response = await client.post(
|
||||
"/api/v2/repositories",
|
||||
json={
|
||||
"name": "main",
|
||||
"relative_path": "second",
|
||||
"compression": "none",
|
||||
"encryption": "none",
|
||||
},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
assert response.status_code == 409
|
||||
assert not (settings.repository_roots[0] / "main").exists()
|
||||
assert not (settings.repository_roots[0] / "second").exists()
|
||||
await app.state.engine.dispose()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user