agromatrix: enforce mentor auth and expose shared-memory review via gateway
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -17,6 +19,14 @@ router = APIRouter(prefix="/v1", tags=["daarion-facade"])
|
||||
EVENT_TERMINAL_STATUSES = {"done", "failed"}
|
||||
EVENT_KNOWN_STATUSES = {"queued", "running", "done", "failed"}
|
||||
EVENT_POLL_SECONDS = float(os.getenv("DAARION_JOB_EVENTS_POLL_SECONDS", "0.5"))
|
||||
ROUTER_URL = os.getenv("ROUTER_URL", "http://router:8000").rstrip("/")
|
||||
ROUTER_REVIEW_TIMEOUT = float(os.getenv("DAARION_ROUTER_REVIEW_TIMEOUT_SECONDS", "20"))
|
||||
AGROMATRIX_REVIEW_AUTH_MODE = os.getenv("AGROMATRIX_REVIEW_AUTH_MODE", "bearer").strip().lower()
|
||||
AGROMATRIX_REVIEW_BEARER_TOKENS = [
|
||||
part.strip()
|
||||
for part in os.getenv("AGROMATRIX_REVIEW_BEARER_TOKENS", "").replace(";", ",").split(",")
|
||||
if part.strip()
|
||||
]
|
||||
|
||||
|
||||
class InvokeInput(BaseModel):
|
||||
@@ -36,6 +46,69 @@ class InvokeResponse(BaseModel):
|
||||
status_url: str
|
||||
|
||||
|
||||
class SharedMemoryReviewRequest(BaseModel):
|
||||
point_id: str
|
||||
approve: bool
|
||||
reviewer: str | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
def _extract_bearer_token(request: Request) -> str:
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing Bearer token")
|
||||
token = auth_header[len("Bearer ") :].strip()
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="Empty Bearer token")
|
||||
return token
|
||||
|
||||
|
||||
def _require_mentor_auth(request: Request) -> str:
|
||||
mode = AGROMATRIX_REVIEW_AUTH_MODE
|
||||
if mode in {"off", "none", "disabled"}:
|
||||
return ""
|
||||
if mode != "bearer":
|
||||
raise HTTPException(status_code=500, detail=f"Unsupported AGROMATRIX_REVIEW_AUTH_MODE={mode}")
|
||||
if not AGROMATRIX_REVIEW_BEARER_TOKENS:
|
||||
raise HTTPException(status_code=503, detail="Review auth is not configured")
|
||||
token = _extract_bearer_token(request)
|
||||
if not any(hmac.compare_digest(token, candidate) for candidate in AGROMATRIX_REVIEW_BEARER_TOKENS):
|
||||
raise HTTPException(status_code=403, detail="Invalid mentor token")
|
||||
return token
|
||||
|
||||
|
||||
async def _router_json(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
payload: Dict[str, Any] | None = None,
|
||||
params: Dict[str, Any] | None = None,
|
||||
authorization: str | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
headers: Dict[str, str] = {}
|
||||
if authorization:
|
||||
headers["Authorization"] = authorization
|
||||
url = f"{ROUTER_URL}{path}"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=ROUTER_REVIEW_TIMEOUT) as client:
|
||||
resp = await client.request(method, url, json=payload, params=params, headers=headers)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(status_code=504, detail="Router timeout")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"Router unavailable: {e}")
|
||||
|
||||
try:
|
||||
body = resp.json()
|
||||
except Exception:
|
||||
body = {"raw": resp.text}
|
||||
|
||||
if resp.status_code >= 400:
|
||||
detail = body.get("detail") if isinstance(body, dict) else body
|
||||
raise HTTPException(status_code=resp.status_code, detail=detail or f"Router error {resp.status_code}")
|
||||
return body if isinstance(body, dict) else {"data": body}
|
||||
|
||||
|
||||
def _sse_message(event: str, payload: Dict[str, Any]) -> str:
|
||||
return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
|
||||
@@ -116,3 +189,24 @@ async def job_events(job_id: str, request: Request) -> StreamingResponse:
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agromatrix/shared-memory/pending")
|
||||
async def agromatrix_shared_pending(limit: int = 50) -> Dict[str, Any]:
|
||||
return await _router_json(
|
||||
"GET",
|
||||
"/v1/agromatrix/shared-memory/pending",
|
||||
params={"limit": max(1, min(limit, 200))},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agromatrix/shared-memory/review")
|
||||
async def agromatrix_shared_review(req: SharedMemoryReviewRequest, request: Request) -> Dict[str, Any]:
|
||||
token = _require_mentor_auth(request)
|
||||
auth_header = f"Bearer {token}" if token else None
|
||||
return await _router_json(
|
||||
"POST",
|
||||
"/v1/agromatrix/shared-memory/review",
|
||||
payload=req.model_dump(),
|
||||
authorization=auth_header,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user