Files
gt-ai-os-community/apps/tenant-backend/app/api/v1/webhooks.py
HackWeasel 310491a557 GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents
- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2
- Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2
  - Made more general-purpose (flexible targets, expanded tools)
- Added nemotron-mini-agent.csv for fast local inference via Ollama
- Added nemotron-agent.csv for advanced reasoning via Ollama
- Added wiki page: Projects for NVIDIA NIMs and Nemotron
2025-12-12 17:47:14 -05:00

376 lines
11 KiB
Python

"""
Webhook Receiver API
Handles incoming webhooks for automation triggers with security validation
and rate limiting.
"""
import logging
import hmac
import hashlib
from typing import Optional, Dict, Any
from datetime import datetime
from fastapi import APIRouter, Request, HTTPException, BackgroundTasks, Depends, Header
from pydantic import BaseModel
from app.services.event_bus import TenantEventBus, TriggerType
from app.services.automation_executor import AutomationChainExecutor
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
class WebhookRegistration(BaseModel):
"""Webhook registration model"""
name: str
description: Optional[str] = None
secret: Optional[str] = None
rate_limit: int = 60 # Requests per minute
allowed_ips: Optional[list[str]] = None
events_to_trigger: list[str] = []
class WebhookPayload(BaseModel):
"""Generic webhook payload"""
event: Optional[str] = None
data: Dict[str, Any] = {}
timestamp: Optional[str] = None
# In-memory webhook registry (in production, use database)
webhook_registry: Dict[str, Dict[str, Any]] = {}
# Rate limiting tracker (in production, use Redis)
rate_limiter: Dict[str, list[datetime]] = {}
def validate_webhook_registration(tenant_domain: str, webhook_id: str) -> Dict[str, Any]:
"""Validate webhook is registered"""
key = f"{tenant_domain}:{webhook_id}"
if key not in webhook_registry:
raise HTTPException(status_code=404, detail="Webhook not found")
return webhook_registry[key]
def check_rate_limit(key: str, limit: int) -> bool:
"""Check if request is within rate limit"""
now = datetime.utcnow()
# Get request history
if key not in rate_limiter:
rate_limiter[key] = []
# Remove old requests (older than 1 minute)
rate_limiter[key] = [
ts for ts in rate_limiter[key]
if (now - ts).total_seconds() < 60
]
# Check limit
if len(rate_limiter[key]) >= limit:
return False
# Add current request
rate_limiter[key].append(now)
return True
def validate_hmac_signature(
signature: str,
body: bytes,
secret: str
) -> bool:
"""Validate HMAC signature"""
if not signature or not secret:
return False
# Calculate expected signature
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
# Compare signatures
return hmac.compare_digest(signature, expected)
def sanitize_webhook_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize webhook payload to prevent injection attacks"""
# Remove potentially dangerous keys
dangerous_keys = ["__proto__", "constructor", "prototype"]
def clean_dict(d: Dict[str, Any]) -> Dict[str, Any]:
cleaned = {}
for key, value in d.items():
if key not in dangerous_keys:
if isinstance(value, dict):
cleaned[key] = clean_dict(value)
elif isinstance(value, list):
cleaned[key] = [
clean_dict(item) if isinstance(item, dict) else item
for item in value
]
else:
# Limit string length
if isinstance(value, str) and len(value) > 10000:
cleaned[key] = value[:10000]
else:
cleaned[key] = value
return cleaned
return clean_dict(payload)
@router.post("/{tenant_domain}/{webhook_id}")
async def receive_webhook(
tenant_domain: str,
webhook_id: str,
request: Request,
background_tasks: BackgroundTasks,
x_webhook_signature: Optional[str] = Header(None)
):
"""
Receive webhook and trigger automations.
Note: In production, webhooks terminate at NGINX in DMZ, not directly here.
Traffic flow: Internet → NGINX (DMZ) → OAuth2 Proxy → This endpoint
"""
try:
# Validate webhook registration
webhook = validate_webhook_registration(tenant_domain, webhook_id)
# Check rate limiting
rate_key = f"webhook:{tenant_domain}:{webhook_id}"
if not check_rate_limit(rate_key, webhook.get("rate_limit", 60)):
raise HTTPException(status_code=429, detail="Rate limit exceeded")
# Get request body
body = await request.body()
# Validate signature if configured
if webhook.get("secret"):
if not validate_hmac_signature(x_webhook_signature, body, webhook["secret"]):
raise HTTPException(status_code=401, detail="Invalid signature")
# Check IP whitelist if configured
client_ip = request.client.host
allowed_ips = webhook.get("allowed_ips")
if allowed_ips and client_ip not in allowed_ips:
logger.warning(f"Webhook request from unauthorized IP: {client_ip}")
raise HTTPException(status_code=403, detail="IP not authorized")
# Parse and sanitize payload
try:
payload = await request.json()
payload = sanitize_webhook_payload(payload)
except Exception as e:
logger.error(f"Invalid webhook payload: {e}")
raise HTTPException(status_code=400, detail="Invalid payload format")
# Queue for processing
background_tasks.add_task(
process_webhook_automation,
tenant_domain=tenant_domain,
webhook_id=webhook_id,
payload=payload,
webhook_config=webhook
)
return {
"status": "accepted",
"webhook_id": webhook_id,
"timestamp": datetime.utcnow().isoformat()
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error processing webhook: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
async def process_webhook_automation(
tenant_domain: str,
webhook_id: str,
payload: Dict[str, Any],
webhook_config: Dict[str, Any]
):
"""Process webhook and trigger associated automations"""
try:
# Initialize event bus
event_bus = TenantEventBus(tenant_domain)
# Create webhook event
event_type = payload.get("event", "webhook.received")
# Emit event to trigger automations
await event_bus.emit_event(
event_type=event_type,
user_id=webhook_config.get("owner_id", "system"),
data={
"webhook_id": webhook_id,
"payload": payload,
"source": webhook_config.get("name", "Unknown")
},
metadata={
"trigger_type": TriggerType.WEBHOOK.value,
"webhook_config": webhook_config
}
)
logger.info(f"Webhook processed: {webhook_id}{event_type}")
except Exception as e:
logger.error(f"Error processing webhook automation: {e}")
# Emit failure event
try:
event_bus = TenantEventBus(tenant_domain)
await event_bus.emit_event(
event_type="webhook.failed",
user_id="system",
data={
"webhook_id": webhook_id,
"error": str(e)
}
)
except:
pass
@router.post("/{tenant_domain}/register")
async def register_webhook(
tenant_domain: str,
registration: WebhookRegistration,
user_email: str = "admin@example.com" # In production, get from auth
):
"""Register a new webhook endpoint"""
import secrets
# Generate webhook ID
webhook_id = secrets.token_urlsafe(16)
# Store registration
key = f"{tenant_domain}:{webhook_id}"
webhook_registry[key] = {
"id": webhook_id,
"name": registration.name,
"description": registration.description,
"secret": registration.secret or secrets.token_urlsafe(32),
"rate_limit": registration.rate_limit,
"allowed_ips": registration.allowed_ips,
"events_to_trigger": registration.events_to_trigger,
"owner_id": user_email,
"created_at": datetime.utcnow().isoformat(),
"url": f"/webhooks/{tenant_domain}/{webhook_id}"
}
return {
"webhook_id": webhook_id,
"url": f"/webhooks/{tenant_domain}/{webhook_id}",
"secret": webhook_registry[key]["secret"],
"created_at": webhook_registry[key]["created_at"]
}
@router.get("/{tenant_domain}/list")
async def list_webhooks(
tenant_domain: str,
user_email: str = "admin@example.com" # In production, get from auth
):
"""List registered webhooks for tenant"""
webhooks = []
for key, webhook in webhook_registry.items():
if key.startswith(f"{tenant_domain}:"):
# Only show webhooks owned by user
if webhook.get("owner_id") == user_email:
webhooks.append({
"id": webhook["id"],
"name": webhook["name"],
"description": webhook.get("description"),
"url": webhook["url"],
"rate_limit": webhook["rate_limit"],
"created_at": webhook["created_at"]
})
return {
"webhooks": webhooks,
"total": len(webhooks)
}
@router.delete("/{tenant_domain}/{webhook_id}")
async def delete_webhook(
tenant_domain: str,
webhook_id: str,
user_email: str = "admin@example.com" # In production, get from auth
):
"""Delete a webhook registration"""
key = f"{tenant_domain}:{webhook_id}"
if key not in webhook_registry:
raise HTTPException(status_code=404, detail="Webhook not found")
webhook = webhook_registry[key]
# Check ownership
if webhook.get("owner_id") != user_email:
raise HTTPException(status_code=403, detail="Not authorized")
# Delete webhook
del webhook_registry[key]
return {
"status": "deleted",
"webhook_id": webhook_id
}
@router.get("/{tenant_domain}/{webhook_id}/test")
async def test_webhook(
tenant_domain: str,
webhook_id: str,
background_tasks: BackgroundTasks,
user_email: str = "admin@example.com" # In production, get from auth
):
"""Send a test payload to webhook"""
key = f"{tenant_domain}:{webhook_id}"
if key not in webhook_registry:
raise HTTPException(status_code=404, detail="Webhook not found")
webhook = webhook_registry[key]
# Check ownership
if webhook.get("owner_id") != user_email:
raise HTTPException(status_code=403, detail="Not authorized")
# Create test payload
test_payload = {
"event": "webhook.test",
"data": {
"message": "This is a test webhook",
"timestamp": datetime.utcnow().isoformat()
}
}
# Process webhook
background_tasks.add_task(
process_webhook_automation,
tenant_domain=tenant_domain,
webhook_id=webhook_id,
payload=test_payload,
webhook_config=webhook
)
return {
"status": "test_sent",
"webhook_id": webhook_id,
"payload": test_payload
}