Enforce deployment security boundaries
This commit is contained in:
@@ -4,8 +4,9 @@ import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.audit.logging import audit_event, audit_operation_context
|
||||
@@ -142,6 +143,75 @@ class CoreEventTests(unittest.TestCase):
|
||||
cors_origins=("*",),
|
||||
)
|
||||
|
||||
def test_app_sets_safe_default_browser_headers_and_https_hsts(self) -> None:
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{"APP_ENV": "test", "GOVOPLAN_HTTP_HSTS_SECONDS": "86400"},
|
||||
):
|
||||
app = create_govoplan_app(
|
||||
title="security header test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
)
|
||||
|
||||
with TestClient(app, base_url="https://govoplan.example.test") as client:
|
||||
response = client.get("/health")
|
||||
|
||||
self.assertEqual("nosniff", response.headers["X-Content-Type-Options"])
|
||||
self.assertEqual("DENY", response.headers["X-Frame-Options"])
|
||||
self.assertEqual("strict-origin-when-cross-origin", response.headers["Referrer-Policy"])
|
||||
self.assertIn("frame-ancestors 'none'", response.headers["Content-Security-Policy"])
|
||||
self.assertEqual("max-age=86400", response.headers["Strict-Transport-Security"])
|
||||
|
||||
def test_production_app_startup_rejects_an_unvalidated_environment(self) -> None:
|
||||
with patch.dict("os.environ", {"APP_ENV": "production"}, clear=True), self.assertRaisesRegex(
|
||||
RuntimeError,
|
||||
"GovOPlaN configuration validation: FAILED",
|
||||
):
|
||||
create_govoplan_app(
|
||||
title="unsafe production test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
)
|
||||
|
||||
def test_app_rejects_request_bodies_above_deployment_limit(self) -> None:
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/body")
|
||||
async def body(request: Request):
|
||||
return {"size": len(await request.body())}
|
||||
|
||||
with patch.dict("os.environ", {"GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES": "10"}):
|
||||
app = create_govoplan_app(
|
||||
title="request body limit test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
api_router=router,
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/body", content=b"12345678901")
|
||||
streamed = client.post("/body", content=(chunk for chunk in (b"123456", b"78901")))
|
||||
|
||||
self.assertEqual(413, response.status_code, response.text)
|
||||
self.assertIn("10 bytes", response.json()["detail"])
|
||||
self.assertEqual(413, streamed.status_code, streamed.text)
|
||||
|
||||
def test_app_enforces_configured_trusted_hosts(self) -> None:
|
||||
with patch.dict("os.environ", {"GOVOPLAN_TRUSTED_HOSTS": "govoplan.example.test,*.internal.test"}):
|
||||
app = create_govoplan_app(
|
||||
title="trusted host test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
)
|
||||
|
||||
with TestClient(app, base_url="https://govoplan.example.test") as client:
|
||||
allowed = client.get("/health")
|
||||
rejected = client.get("https://attacker.example.test/health")
|
||||
|
||||
self.assertEqual(200, allowed.status_code, allowed.text)
|
||||
self.assertEqual(400, rejected.status_code, rejected.text)
|
||||
|
||||
def test_audit_event_persists_trace_details_from_event_context(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-audit-trace-"))
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user