fix: write-ahead audit entry so calls stop when the audit log stops accepting writes (SEC-03)
This commit is contained in:
1 parent
5b622e64e2
commit
43eebd3089
8 files changed
+134
-15
No files matched your search
@@ -0,0 +1,79 @@
|
||||
"""The audit log is fail closed after startup too (SEC-03) and names the client (SEC-12)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pytest_httpx import HTTPXMock
|
||||
|
||||
from aegis_gitea_mcp.audit import get_audit_logger
|
||||
from tests.test_server_helpers import API, connected, mock_user
|
||||
|
||||
REPO_ARGS = {"owner": "octo", "repo": "demo"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _env(mock_env: None) -> None:
|
||||
"""The audit logger reads the process-wide settings."""
|
||||
|
||||
|
||||
class FailingFile:
|
||||
"""Stands in for the open audit file and fails every write like a full disk."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.attempts = 0
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
self.attempts += 1
|
||||
raise OSError(28, "No space left on device")
|
||||
|
||||
def flush(self) -> None: # pragma: no cover - never reached
|
||||
raise OSError(28, "No space left on device")
|
||||
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def payload_of(result: Any) -> dict[str, Any]:
|
||||
decoded: dict[str, Any] = json.loads(result.content[0].text)
|
||||
return decoded
|
||||
|
||||
|
||||
async def test_calls_stop_when_the_audit_log_stops_accepting_writes(
|
||||
httpx_mock: HTTPXMock,
|
||||
) -> None:
|
||||
mock_user(httpx_mock)
|
||||
httpx_mock.add_response(url=f"{API}/repos/octo/demo", json={"name": "demo"}, is_reusable=True)
|
||||
async with connected() as client:
|
||||
first = await client.call_tool("gitea_repo_get", REPO_ARGS)
|
||||
assert not first.is_error
|
||||
sent_before = len(httpx_mock.get_requests())
|
||||
|
||||
audit = get_audit_logger()
|
||||
healthy_file = audit._log_file
|
||||
audit._log_file = FailingFile() # type: ignore[assignment]
|
||||
second = await client.call_tool("gitea_repo_get", REPO_ARGS)
|
||||
mutation = await client.call_tool(
|
||||
"gitea_issue_create_issue", {**REPO_ARGS, "body": {"title": "x"}}
|
||||
)
|
||||
|
||||
assert second.is_error and mutation.is_error
|
||||
assert payload_of(second)["error"]["kind"] == "audit_unavailable"
|
||||
assert payload_of(mutation)["error"]["kind"] == "audit_unavailable"
|
||||
# Gitea was not contacted at all while the log was broken.
|
||||
assert len(httpx_mock.get_requests()) == sent_before
|
||||
|
||||
audit._log_file = healthy_file
|
||||
resumed = await client.call_tool("gitea_repo_get", REPO_ARGS)
|
||||
assert not resumed.is_error
|
||||
|
||||
|
||||
async def test_the_refusal_does_not_leak_the_failure_reason() -> None:
|
||||
async with connected() as client:
|
||||
get_audit_logger()._log_file = FailingFile() # type: ignore[assignment]
|
||||
result = await client.call_tool("gitea_repo_get", REPO_ARGS)
|
||||
|
||||
text = result.content[0].text # type: ignore[union-attr]
|
||||
assert "No space left" not in text and "OSError" not in text
|
||||
Reference in new issue
Block a user