- Add WEBHOOK_CLIENT_SECRET validation for Graph webhooks - Add Redis-backed rate limiting (fetch/ask/write/default tiers) - Validate LLM_BASE_URL to prevent SSRF (HTTPS only, block private IPs) - Enforce non-wildcard CORS when AUTH_ENABLED=true - Add Content-Security-Policy headers - Fix audit middleware to use verified JWT claims via contextvars - Cap bulk_tags updates to 10,000 documents - Return generic error messages to clients (no internal detail leakage) - Strict AlertCondition Pydantic model for alert rules - Security warning on MCP stdio server startup - Remove MongoDB/Redis host ports from docker-compose - Remove mongo_query from /ask API response
51 lines
1.8 KiB
Python
51 lines
1.8 KiB
Python
import structlog
|
|
from config import WEBHOOK_CLIENT_SECRET
|
|
from fastapi import APIRouter, Request, Response
|
|
|
|
router = APIRouter()
|
|
logger = structlog.get_logger("aoc.webhooks")
|
|
|
|
|
|
@router.post("/webhooks/graph")
|
|
async def graph_webhook(request: Request):
|
|
"""
|
|
Receive Microsoft Graph change notifications.
|
|
Handles the validation handshake by echoing validationToken.
|
|
Validates clientState on notifications to prevent spoofing.
|
|
"""
|
|
validation_token = request.query_params.get("validationToken")
|
|
if validation_token:
|
|
# Microsoft sends validationToken as a query param during subscription creation.
|
|
# Echo it back as plain text to prove endpoint ownership.
|
|
return Response(content=validation_token, media_type="text/plain")
|
|
|
|
try:
|
|
body = await request.json()
|
|
except Exception as exc:
|
|
logger.warning("Invalid webhook payload", error=str(exc))
|
|
return Response(status_code=400)
|
|
|
|
notifications = body.get("value", [])
|
|
if not isinstance(notifications, list):
|
|
logger.warning("Invalid webhook payload structure")
|
|
return Response(status_code=400)
|
|
|
|
for notification in notifications:
|
|
client_state = notification.get("clientState")
|
|
if WEBHOOK_CLIENT_SECRET and client_state != WEBHOOK_CLIENT_SECRET:
|
|
logger.warning(
|
|
"Graph webhook rejected: invalid clientState",
|
|
change_type=notification.get("changeType"),
|
|
resource=notification.get("resource"),
|
|
)
|
|
return Response(status_code=401)
|
|
|
|
logger.info(
|
|
"Received Graph notification",
|
|
change_type=notification.get("changeType"),
|
|
resource=notification.get("resource"),
|
|
client_state=client_state,
|
|
)
|
|
|
|
return {"status": "accepted"}
|