Repository: ethyca/fideslog (language: python)

Issue:
Require a custom header on all API server requests
In order to reduce the amount of spam requests handled by the API server, require that all requests include a custom header, like `"X-Fideslog-Version": "1.0.0"`. This doesn't provide any means of authentication or authorization, but will dramatically reduce the number of handled requests by rejecting any that are submitted by bots, etc.


Discussion / hints:

Fix patch:
diff --git a/fideslog/api/main.py b/fideslog/api/main.py
index a163ff5..b3ce463 100644
--- a/fideslog/api/main.py
+++ b/fideslog/api/main.py
@@ -3,7 +3,8 @@
 from http import HTTPStatus
 from typing import Callable
 
-from fastapi import FastAPI, Request, Response
+from fastapi import FastAPI, Request, Response, status
+from fastapi.responses import JSONResponse
 from uvicorn import run
 
 from fideslog.api.config import ServerSettings, config
@@ -15,9 +16,35 @@
 app.include_router(api_router)
 
 
+# Defined before `log_request` to ensure that both are always executed.
+@app.middleware("http")
+async def require_version_header(request: Request, call_next: Callable) -> Response:
+    """
+    Enforce that the `X-Fideslog-Version` header was included on the request.
+    Does not apply to the `/docs` and `/openapi.json` endpoints, to ensure
+    that they remain publicly available.
+
+    This header is intentionally undocumented, for mildly increased security.
+    """
+
+    excluded_endpoints = ["/docs", "/openapi.json"]
+    version = request.headers.get("x-fideslog-version", None)
+
+    if version is None and request.url.path not in excluded_endpoints:
+        return JSONResponse(
+            {"error": "Missing required header(s)"},
+            status.HTTP_400_BAD_REQUEST,
+        )
+
+    return await call_next(request)
+
+
 @app.middleware("http")
 async def log_request(request: Request, call_next: Callable) -> Response:
-    "Log basic information about every request handled by the server."
+    """
+    Log basic information about every request handled by the server.
+    """
+
     start = datetime.now()
     response = await call_next(request)
     handler_time = round((datetime.now() - start).microseconds * 0.001, 3)
diff --git a/fideslog/sdk/python/client.py b/fideslog/sdk/python/client.py
index 0915a58..6dc3f66 100644
--- a/fideslog/sdk/python/client.py
+++ b/fideslog/sdk/python/client.py
@@ -5,9 +5,12 @@
 
 from aiohttp import ClientResponseError, ClientSession, ClientTimeout
 
+import fideslog.sdk.python as fideslog
 from fideslog.sdk.python.event import AnalyticsEvent
 from fideslog.sdk.python.exceptions import AnalyticsException
 
+REQUIRED_HEADERS = {"X-Fideslog-Version": fideslog.__version__}
+
 
 class AnalyticsClient:
     """
@@ -98,6 +101,7 @@ async def __send(self, event: AnalyticsEvent) -> None:
 
         async with ClientSession(
             self.server_url,
+            headers=REQUIRED_HEADERS,
             timeout=ClientTimeout(connect=3.05, total=120),
         ) as session:
             async with session.post(


Test patch:
diff --git a/tests/api/test_routes.py b/tests/api/test_routes.py
index 51cc979..ae18997 100644
--- a/tests/api/test_routes.py
+++ b/tests/api/test_routes.py
@@ -9,7 +9,7 @@
 def test_health() -> None:
     """Test that the /health endpoint responds"""
 
-    response = client.get("/health")
+    response = client.get("/health", headers={"X-Fideslog-Version": "1.0.0"})
 
     assert response.status_code == status.HTTP_200_OK
     assert response.json() == {"status": "healthy"}