72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
import httpx
|
|
import pytest
|
|
|
|
from src import main
|
|
|
|
|
|
REQUIRED_HEADERS = {
|
|
"x-content-type-options": "nosniff",
|
|
"referrer-policy": "no-referrer",
|
|
"x-frame-options": "DENY",
|
|
}
|
|
|
|
|
|
def assert_browser_security_boundary(response: httpx.Response) -> None:
|
|
for name, value in REQUIRED_HEADERS.items():
|
|
assert response.headers[name] == value
|
|
assert "camera=()" in response.headers["permissions-policy"]
|
|
policy = response.headers["content-security-policy"]
|
|
assert "default-src 'self'" in policy
|
|
assert "script-src 'self'" in policy
|
|
assert "object-src 'none'" in policy
|
|
assert "frame-ancestors 'none'" in policy
|
|
script_policy = next(part for part in policy.split(";") if "script-src" in part)
|
|
assert "'unsafe-inline'" not in script_policy
|
|
assert "'unsafe-eval'" not in script_policy
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_security_boundary_covers_pages_health_static_and_api_errors(monkeypatch):
|
|
monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "access-token-with-at-least-thirty-two-characters")
|
|
monkeypatch.setenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", "session-secret-with-at-least-thirty-two-characters")
|
|
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
|
|
monkeypatch.setenv("STACKCHAIN_DASHBOARD_PUBLIC_ORIGIN", "https://test")
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
|
|
responses = [
|
|
await client.get("/healthz"),
|
|
await client.get("/login"),
|
|
await client.get("/static/dashboard.js"),
|
|
await client.get("/api/v1/context"),
|
|
]
|
|
|
|
assert [response.status_code for response in responses] == [200, 200, 200, 401]
|
|
for response in responses:
|
|
assert_browser_security_boundary(response)
|
|
assert response.headers["strict-transport-security"] == "max-age=31536000"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_security_boundary_covers_fail_closed_authentication_response(monkeypatch):
|
|
monkeypatch.delenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", raising=False)
|
|
monkeypatch.delenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", raising=False)
|
|
monkeypatch.delenv("STACKCHAIN_DASHBOARD_AUTH_MODE", raising=False)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
|
|
response = await client.get("/login")
|
|
|
|
assert response.status_code == 503
|
|
assert_browser_security_boundary(response)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_insecure_local_mode_does_not_emit_hsts():
|
|
transport = httpx.ASGITransport(app=main.app, client=("127.0.0.1", 123))
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://localhost") as client:
|
|
response = await client.get("/healthz")
|
|
|
|
assert response.status_code == 200
|
|
assert "strict-transport-security" not in response.headers
|