[AZ-178] Fix Critical/High security findings: auth, CVEs, non-root containers, per-job SSE

- Pin all deps; h11==0.16.0 (CVE-2025-43859), python-multipart>=1.3.1 (CVE-2026-28356), PyJWT==2.12.1
- Add HMAC JWT verification (require_auth FastAPI dependency, JWT_SECRET-gated)
- Fix TokenManager._refresh() to use ADMIN_API_URL instead of ANNOTATIONS_URL
- Rename POST /detect → POST /detect/image (image-only, rejects video files)
- Replace global SSE stream with per-job SSE: GET /detect/{media_id} with event replay buffer
- Apply require_auth to all 4 protected endpoints
- Fix on_annotation/on_status closure to use mutable current_id for correct post-upload event routing
- Add non-root appuser to Dockerfile and Dockerfile.gpu
- Add JWT_SECRET to e2e/docker-compose.test.yml and run-tests.sh
- Update all e2e tests and unit tests for new endpoints and HMAC token signing
- 64/64 tests pass

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-02 06:32:12 +03:00
parent dac350cbc5
commit 097811a67b
25 changed files with 369 additions and 429 deletions
+19 -12
View File
@@ -81,10 +81,11 @@ def _weather_label_ok(label, base_names):
@pytest.mark.slow
def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm_engine):
def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm_engine, auth_headers):
r = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
headers=auth_headers,
)
assert r.status_code == 200
body = r.json()
@@ -105,12 +106,13 @@ def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm
@pytest.mark.slow
def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine):
def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine, auth_headers):
cfg_hi = json.dumps({"probability_threshold": 0.8})
r_hi = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg_hi},
headers=auth_headers,
)
assert r_hi.status_code == 200
hi = r_hi.json()
@@ -119,9 +121,10 @@ def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine)
assert float(d["confidence"]) + _EPS >= 0.8
cfg_lo = json.dumps({"probability_threshold": 0.1})
r_lo = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg_lo},
headers=auth_headers,
)
assert r_lo.status_code == 200
lo = r_lo.json()
@@ -130,12 +133,13 @@ def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine)
@pytest.mark.slow
def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine):
def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine, auth_headers):
cfg_loose = json.dumps({"tracking_intersection_threshold": 0.6})
r1 = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_dense, "image/jpeg")},
data={"config": cfg_loose},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r1.status_code == 200
@@ -151,9 +155,10 @@ def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine
assert ratio <= 0.6 + _EPS, (label, ratio)
cfg_strict = json.dumps({"tracking_intersection_threshold": 0.01})
r2 = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_dense, "image/jpeg")},
data={"config": cfg_strict},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r2.status_code == 200
@@ -163,7 +168,7 @@ def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine
@pytest.mark.slow
def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engine):
def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engine, auth_headers):
by_id, _ = _load_classes_media()
wh = _image_width_height(image_small)
assert wh is not None
@@ -180,9 +185,10 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
}
)
r = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r.status_code == 200
@@ -197,12 +203,13 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
@pytest.mark.slow
def test_ft_p_13_weather_mode_class_variants_ac5(
http_client, image_different_types, warm_engine
http_client, image_different_types, warm_engine, auth_headers
):
_, base_names = _load_classes_media()
r = http_client.post(
"/detect",
"/detect/image",
files={"file": ("img.jpg", image_different_types, "image/jpeg")},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r.status_code == 200