mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 10:56:32 +00:00
[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:
+28
-22
@@ -1,11 +1,10 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import jwt as pyjwt
|
||||
import pytest
|
||||
import requests
|
||||
import sseclient
|
||||
@@ -55,11 +54,33 @@ def http_client(base_url):
|
||||
return _SessionWithBase(base_url, 30)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jwt_secret():
|
||||
return os.environ.get("JWT_SECRET", "")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jwt_token(jwt_secret):
|
||||
if not jwt_secret:
|
||||
return ""
|
||||
return pyjwt.encode(
|
||||
{"sub": "test-user", "exp": int(time.time()) + 3600},
|
||||
jwt_secret,
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth_headers(jwt_token):
|
||||
return {"Authorization": f"Bearer {jwt_token}"} if jwt_token else {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sse_client_factory(http_client):
|
||||
def sse_client_factory(http_client, auth_headers):
|
||||
@contextmanager
|
||||
def _open():
|
||||
with http_client.get("/detect/stream", stream=True, timeout=600) as resp:
|
||||
def _open(media_id: str):
|
||||
with http_client.get(f"/detect/{media_id}", stream=True,
|
||||
timeout=600, headers=auth_headers) as resp:
|
||||
resp.raise_for_status()
|
||||
yield sseclient.SSEClient(resp)
|
||||
|
||||
@@ -180,31 +201,16 @@ def corrupt_image():
|
||||
return random.randbytes(1024)
|
||||
|
||||
|
||||
def _b64url_obj(obj: dict) -> str:
|
||||
raw = json.dumps(obj, separators=(",", ":")).encode()
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jwt_token():
|
||||
header = (
|
||||
base64.urlsafe_b64encode(json.dumps({"alg": "none", "typ": "JWT"}).encode())
|
||||
.decode()
|
||||
.rstrip("=")
|
||||
)
|
||||
payload = _b64url_obj({"exp": int(time.time()) + 3600, "sub": "test"})
|
||||
return f"{header}.{payload}.signature"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def warm_engine(http_client, image_small):
|
||||
def warm_engine(http_client, image_small, auth_headers):
|
||||
deadline = time.time() + 120
|
||||
files = {"file": ("warm.jpg", image_small, "image/jpeg")}
|
||||
consecutive_errors = 0
|
||||
last_status = None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
r = http_client.post("/detect", files=files)
|
||||
r = http_client.post("/detect/image", files=files, headers=auth_headers)
|
||||
if r.status_code == 200:
|
||||
return
|
||||
last_status = r.status_code
|
||||
|
||||
Reference in New Issue
Block a user