Files
ss-tools/backend/src/core/middleware/trace.py
busya a9a453109c security: critical auth fixes, test migration to SQLite+FK, 44 test fixes
CRITICAL (CVE-class):
- C-4: Remove DEV_MODE fallback with hardcoded postgres:postgres credentials
- C-3: WebSocket endpoints now require JWT or API key token (?token=) auth
- C-1: Superset environment passwords encrypted via Fernet at rest in DB
- H-4: SESSION_SECRET_KEY separated from JWT AUTH_SECRET_KEY
- H-1: Log injection via %s-formatting instead of f-strings
- H-5: X-Trace-ID header validated as UUID4 to prevent trace poisoning

INFRASTRUCTURE:
- New src/core/encryption.py — EncryptionManager extracted from llm_provider
- Test DB: per-module sqlite:///:memory: with PRAGMA foreign_keys=ON
- testcontainers PostgreSQL via TEST_DB=postgres env var
- conftest temp-file global engine (no 10GB shared-cache leak)

TEST FIXES (44 pre-existing → 0):
- test_smoke_plugins: module-level sys.modules mock isolated to per-test fixture
- test_migration_engine: EXT:Python:uuid → uuid syntax fix
- test_dashboards_api: mock env attributes + correct patch target
- test_constants_audit_fixes: sync expected constant names with actual
- test_defensive_guards: patch.object instead of module-level Repo mock
- test_clean_release_cli: removed empty config.json directory
- FK violations: TaskRecord parents in log_persistence, Environment in
  mapping_service, ReleaseCandidate in candidate_manifest_services

ORTHOGONAL TESTS (18 new):
- test_security_orthogonal.py: bcrypt 72-byte limit, unicode, API key
  format invariants, Fernet robustness, encryption key lifecycle,
  log injection protection, JWT edge cases

MODEL FIXES:
- MetricSnapshot.job_id: nullable with SET NULL (was broken FK for
  aggregate prune snapshots, hidden by SQLite)

CLEANUP:
- Removed stale :memory:test_main/test_auth/test_tasks file databases
- Removed duplicate #endregion in encryption_key.py
2026-05-26 14:58:49 +03:00

83 lines
3.3 KiB
Python

# #region TraceContextMiddlewareModule [C:3] [TYPE Module] [SEMANTICS fastapi, middleware, trace, context, request]
# @BRIEF FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request.
# Optionally extracts X-Trace-ID header for cross-service trace propagation.
# @LAYER Core
# @RELATION DEPENDS_ON -> [CotLoggerModule]
# @RELATION CALLED_BY -> [AppModule]
# @INVARIANT X-Trace-ID header is validated as UUID4 format to prevent log injection / trace poisoning.
# @PRE FastAPI app instance with Starlette middleware support.
# @POST Every request gets a trace_id via seed_trace_id(). Existing X-Trace-ID header is
# preserved and used as the trace_id when present and valid.
# @SIDE_EFFECT Sets ContextVar _trace_id for the duration of the request.
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import ASGIApp
from ..cot_logger import seed_trace_id, set_trace_id
# #region TraceContextMiddleware [C:2] [TYPE Class] [SEMANTICS middleware, trace, context, dispatch]
# @BRIEF Starlette BaseHTTPMiddleware that seeds a trace_id per request.
class TraceContextMiddleware(BaseHTTPMiddleware):
"""FastAPI/Starlette middleware that seeds a trace_id for every request.
If the incoming request carries an ``X-Trace-ID`` header, that value is
validated as UUID4 and used as the trace_id (enabling cross-service trace
chaining). Invalid or non-UUID values are ignored and a new UUID4 is
generated instead.
Usage::
from backend.src.core.middleware.trace import TraceContextMiddleware
app.add_middleware(TraceContextMiddleware)
"""
# #region TraceContextMiddleware.__init__ [TYPE Function]
# @BRIEF Standard BaseHTTPMiddleware initialiser.
def __init__(self, app: ASGIApp) -> None:
super().__init__(app)
# #endregion TraceContextMiddleware.__init__
# #region TraceContextMiddleware.dispatch [C:2] [TYPE Function] [SEMANTICS dispatch, trace, seed, header, call_next]
# @BRIEF Dispatch handler that seeds trace_id before passing to the next middleware.
async def dispatch(
self, request: Request, call_next
) -> Response:
"""Intercept every request, seed the trace_id, and forward.
If the client sent an ``X-Trace-ID`` header with a valid UUID4 value,
use it; otherwise ``seed_trace_id()`` generates a new UUID4.
Invalid X-Trace-ID values are silently ignored to prevent log
injection / trace poisoning (see [SEC:H-5]).
Args:
request: The incoming Starlette Request.
call_next: The next middleware or route handler.
Returns:
Response from the downstream handler.
"""
incoming_trace_id = request.headers.get("X-Trace-ID")
if incoming_trace_id:
try:
uuid.UUID(hex=incoming_trace_id, version=4)
set_trace_id(incoming_trace_id)
except (ValueError, AttributeError):
seed_trace_id()
else:
seed_trace_id()
response = await call_next(request)
return response
# #endregion TraceContextMiddleware.dispatch
# #endregion TraceContextMiddleware
# #endregion TraceContextMiddlewareModule