fix(history): reject invalid inspection targets (#209)
This commit is contained in:
@@ -4,11 +4,13 @@ import asyncio
|
||||
import errno
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import threading
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import skipUnless
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiohttp import web
|
||||
@@ -1088,3 +1090,150 @@ class HistoryOutputAPITests(ImageAPITestCase):
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 503, "storage-unavailable")
|
||||
self.assertEqual(body["recovery"], {"action": "retry-read"})
|
||||
|
||||
async def test_inspection_rejects_directory_replacement_before_parser(self):
|
||||
path = png(self.output_dir / "image.png", prompt=graph("Current portrait"))
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assertEqual(response.status, 200, body)
|
||||
self.assertEqual(body["prompts"]["positive"]["text"], "Current portrait")
|
||||
service = importlib.import_module("py.api.inspection_service")
|
||||
inspect = service.inspect_current_file
|
||||
read = service.read_inspection_file
|
||||
reads = []
|
||||
|
||||
def replaced(target, **kwargs):
|
||||
path.unlink()
|
||||
path.mkdir()
|
||||
return inspect(target, **kwargs)
|
||||
|
||||
def traced(target):
|
||||
reads.append(target)
|
||||
return read(target)
|
||||
|
||||
with patch("py.api.history.inspect_current_file", side_effect=replaced):
|
||||
with patch.object(service, "read_inspection_file", side_effect=traced):
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 409, "candidate-stale")
|
||||
self.assertEqual(reads, [], "Nonregular targets must not reach the parser")
|
||||
self.assertTrue(path.is_dir())
|
||||
self.assertNotIn("Current portrait", json.dumps(body))
|
||||
|
||||
@skipUnless(os.name == "posix", "FIFO replacement is a POSIX filesystem case")
|
||||
async def test_inspection_rejects_fifo_replacement_before_parser(self):
|
||||
path = make_png(self.output_dir / "image.png")
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
self.assertEqual(response.status, 200, await response.json())
|
||||
service = importlib.import_module("py.api.inspection_service")
|
||||
inspect = service.inspect_current_file
|
||||
reads = []
|
||||
|
||||
def replaced(target, **kwargs):
|
||||
path.unlink()
|
||||
os.mkfifo(path)
|
||||
return inspect(target, **kwargs)
|
||||
|
||||
def forbidden(target):
|
||||
reads.append(target)
|
||||
raise AssertionError("FIFO must be rejected before a metadata open")
|
||||
|
||||
with patch("py.api.history.inspect_current_file", side_effect=replaced):
|
||||
with patch.object(service, "read_inspection_file", side_effect=forbidden):
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 409, "candidate-stale")
|
||||
self.assertEqual(reads, [], "A FIFO must never reach the blocking parser")
|
||||
|
||||
async def test_inspection_rejects_unreadable_file_before_parser(self):
|
||||
path = png(self.output_dir / "image.png", prompt=graph("Readable portrait"))
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
service = importlib.import_module("py.api.inspection_service")
|
||||
inspect = service.inspect_current_file
|
||||
read = service.read_inspection_file
|
||||
access = os.access
|
||||
revoked = False
|
||||
reads = []
|
||||
|
||||
def unreadable(target, **kwargs):
|
||||
nonlocal revoked
|
||||
revoked = True
|
||||
return inspect(target, **kwargs)
|
||||
|
||||
def current_access(target, mode, **kwargs):
|
||||
if revoked and Path(target) == path:
|
||||
return False
|
||||
return access(target, mode, **kwargs)
|
||||
|
||||
def traced(target):
|
||||
reads.append(target)
|
||||
return read(target)
|
||||
|
||||
with patch("py.api.history.inspect_current_file", side_effect=unreadable):
|
||||
with patch("os.access", side_effect=current_access):
|
||||
with patch.object(service, "read_inspection_file", side_effect=traced):
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 409, "candidate-stale")
|
||||
self.assertEqual(reads, [], "Unreadable targets must not reach the parser")
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assertEqual(response.status, 200, body)
|
||||
self.assertEqual(body["prompts"]["positive"]["text"], "Readable portrait")
|
||||
|
||||
async def test_inspection_rejects_directory_replacement_after_parser(self):
|
||||
path = png(self.output_dir / "image.png", prompt=graph("Current portrait"))
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
service = importlib.import_module("py.api.inspection_service")
|
||||
read = service.read_inspection_file
|
||||
|
||||
def replaced(target):
|
||||
result = read(target)
|
||||
path.unlink()
|
||||
path.mkdir()
|
||||
return result
|
||||
|
||||
with patch.object(service, "read_inspection_file", side_effect=replaced):
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 409, "candidate-stale")
|
||||
self.assertNotIn("Current portrait", json.dumps(body))
|
||||
|
||||
async def test_inspection_rejects_readability_revoked_after_parser(self):
|
||||
path = png(self.output_dir / "image.png", prompt=graph("Readable portrait"))
|
||||
before = path.read_bytes()
|
||||
start = self.capture([self.ref()])
|
||||
descriptor = await self.media(start)
|
||||
service = importlib.import_module("py.api.inspection_service")
|
||||
read = service.read_inspection_file
|
||||
access = os.access
|
||||
revoked = False
|
||||
|
||||
def changed(target):
|
||||
nonlocal revoked
|
||||
result = read(target)
|
||||
revoked = True
|
||||
return result
|
||||
|
||||
def current_access(target, mode, **kwargs):
|
||||
if revoked and Path(target) == path:
|
||||
return False
|
||||
return access(target, mode, **kwargs)
|
||||
|
||||
with patch.object(service, "read_inspection_file", side_effect=changed):
|
||||
with patch("os.access", side_effect=current_access):
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assert_error(response, body, 409, "candidate-stale")
|
||||
self.assertNotIn("Readable portrait", json.dumps(body))
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
response = await self.current(descriptor["inspection_url"])
|
||||
body = await response.json()
|
||||
self.assertEqual(response.status, 200, body)
|
||||
self.assertEqual(body["prompts"]["positive"]["text"], "Readable portrait")
|
||||
|
||||
@@ -408,6 +408,33 @@ class CurrentLocationTests(unittest.TestCase):
|
||||
self.assertIsNone(result["library"].get("pair_id"))
|
||||
self.assertIsNone(result["saved"].get("pair"))
|
||||
|
||||
def test_inspection_rejects_unsupported_suffix_before_parser(self):
|
||||
service = self.module("py.api.inspection_service")
|
||||
path = make_png(self.output / "image.png")
|
||||
observed = service.inspect_current_file(
|
||||
path, source_label="output", saved=None, library={"status": "none"}
|
||||
)
|
||||
self.assertEqual(observed["file"]["media_type"], "image")
|
||||
self.assertEqual(observed["inspection"].format, "PNG")
|
||||
unsupported = self.output / "image.txt"
|
||||
unsupported.write_bytes(path.read_bytes())
|
||||
read = service.read_inspection_file
|
||||
reads = []
|
||||
|
||||
def traced(target):
|
||||
reads.append(target)
|
||||
return read(target)
|
||||
|
||||
with patch.object(service, "read_inspection_file", side_effect=traced):
|
||||
with self.assertRaises(service.CurrentFileChanged):
|
||||
service.inspect_current_file(
|
||||
unsupported,
|
||||
source_label="output",
|
||||
saved=None,
|
||||
library={"status": "none"},
|
||||
)
|
||||
self.assertEqual(reads, [], "Unsupported media must not reach the parser")
|
||||
|
||||
|
||||
class HistoryImportTests(unittest.TestCase):
|
||||
def test_api_import_does_not_require_a_running_host_server(self):
|
||||
|
||||
Reference in New Issue
Block a user