Add safe archive preview and extraction workflows
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import hashlib
|
||||
import unittest
|
||||
import zipfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from govoplan_files.backend.routes.uploads import (
|
||||
_validate_archive_preview_token,
|
||||
preview_archive_upload,
|
||||
)
|
||||
from govoplan_files.backend.storage.common import FileStorageError
|
||||
|
||||
|
||||
def _archive_bytes() -> bytes:
|
||||
target = io.BytesIO()
|
||||
with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr("folder/report.txt", b"report")
|
||||
return target.getvalue()
|
||||
|
||||
|
||||
class ArchivePreviewTokenTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.settings = SimpleNamespace(
|
||||
file_upload_zip_max_bytes=10 * 1024 * 1024,
|
||||
file_archive_max_entries=10_000,
|
||||
file_archive_max_expanded_bytes=2 * 1024 * 1024 * 1024,
|
||||
file_archive_max_expansion_ratio=100,
|
||||
file_archive_preview_ttl_seconds=30 * 60,
|
||||
)
|
||||
self.principal = SimpleNamespace(
|
||||
tenant_id="tenant-1",
|
||||
user=SimpleNamespace(id="user-1"),
|
||||
)
|
||||
|
||||
def test_preview_token_is_bound_to_actor_archive_and_destination(self) -> None:
|
||||
archive_data = _archive_bytes()
|
||||
upload = UploadFile(
|
||||
file=io.BytesIO(archive_data),
|
||||
filename="monthly.zip",
|
||||
)
|
||||
with patch(
|
||||
"govoplan_files.backend.routes.uploads.settings",
|
||||
self.settings,
|
||||
):
|
||||
preview = preview_archive_upload(
|
||||
file=upload,
|
||||
owner_type="user",
|
||||
owner_id="user-1",
|
||||
path="incoming",
|
||||
campaign_id=None,
|
||||
password=None,
|
||||
principal=self.principal,
|
||||
)
|
||||
_validate_archive_preview_token(
|
||||
preview.preview_token,
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
owner_type="user",
|
||||
owner_id="user-1",
|
||||
path="incoming",
|
||||
campaign_id=None,
|
||||
archive_format="zip",
|
||||
archive_sha256=hashlib.sha256(archive_data).hexdigest(),
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
FileStorageError, "does not match this upload destination"
|
||||
):
|
||||
_validate_archive_preview_token(
|
||||
preview.preview_token,
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
owner_type="user",
|
||||
owner_id="user-1",
|
||||
path="elsewhere",
|
||||
campaign_id=None,
|
||||
archive_format="zip",
|
||||
archive_sha256=hashlib.sha256(archive_data).hexdigest(),
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
FileStorageError, "contents changed"
|
||||
):
|
||||
_validate_archive_preview_token(
|
||||
preview.preview_token,
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
owner_type="user",
|
||||
owner_id="user-1",
|
||||
path="incoming",
|
||||
campaign_id=None,
|
||||
archive_format="zip",
|
||||
archive_sha256="0" * 64,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user