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
This commit is contained in:
2026-05-26 14:58:49 +03:00
parent f49b7d909e
commit a9a453109c
29 changed files with 928 additions and 176 deletions

View File

@@ -4,11 +4,14 @@
# @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.
# 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
@@ -23,8 +26,9 @@ 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
used as the trace_id (enabling cross-service trace chaining). Otherwise a
new UUID4 is generated.
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::
@@ -46,8 +50,10 @@ class TraceContextMiddleware(BaseHTTPMiddleware):
) -> Response:
"""Intercept every request, seed the trace_id, and forward.
If the client sent an ``X-Trace-ID`` header, use it; otherwise
``seed_trace_id()`` generates a new UUID4.
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.
@@ -58,7 +64,11 @@ class TraceContextMiddleware(BaseHTTPMiddleware):
"""
incoming_trace_id = request.headers.get("X-Trace-ID")
if incoming_trace_id:
set_trace_id(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()