""" Middleware Admin API Endpoints for BreakPilot Provides admin functionality for managing middleware configurations: - View and update middleware settings - Rate limiting IP whitelist/blacklist - View middleware events/statistics """ import os from datetime import datetime, timedelta from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field # Database connection import asyncpg # Session middleware for authentication from session import require_permission, Session router = APIRouter(prefix="/api/admin/middleware", tags=["middleware-admin"]) # Database URL DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql://breakpilot:breakpilot@localhost:5432/breakpilot_dev" ) # Lazy database pool _db_pool: Optional[asyncpg.Pool] = None async def get_db_pool() -> asyncpg.Pool: """Get or create database connection pool.""" global _db_pool if _db_pool is None: _db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10) return _db_pool # ============================================== # Request/Response Models # ============================================== class MiddlewareConfigResponse(BaseModel): """Response model for middleware configuration.""" id: str middleware_name: str enabled: bool config: Dict[str, Any] updated_at: Optional[datetime] = None class MiddlewareConfigUpdateRequest(BaseModel): """Request model for updating middleware configuration.""" enabled: Optional[bool] = None config: Optional[Dict[str, Any]] = None class RateLimitIPRequest(BaseModel): """Request model for adding IP to whitelist/blacklist.""" ip_address: str list_type: str = Field(..., pattern="^(whitelist|blacklist)$") reason: Optional[str] = None expires_at: Optional[datetime] = None class RateLimitIPResponse(BaseModel): """Response model for rate limit IP entry.""" id: str ip_address: str list_type: str reason: Optional[str] = None expires_at: Optional[datetime] = None created_at: datetime class MiddlewareEventResponse(BaseModel): """Response model for middleware event.""" id: str middleware_name: str event_type: str ip_address: Optional[str] = None user_id: Optional[str] = None request_path: Optional[str] = None request_method: Optional[str] = None details: Optional[Dict[str, Any]] = None created_at: datetime class MiddlewareStatsResponse(BaseModel): """Response model for middleware statistics.""" middleware_name: str total_events: int events_last_hour: int events_last_24h: int top_event_types: List[Dict[str, Any]] top_ips: List[Dict[str, Any]] # ============================================== # Middleware Configuration Endpoints # ============================================== @router.get("", response_model=List[MiddlewareConfigResponse]) async def list_middleware_configs( session: Session = Depends(require_permission("settings:read")), ): """ List all middleware configurations. Requires: settings:read permission """ pool = await get_db_pool() rows = await pool.fetch(""" SELECT id, middleware_name, enabled, config, updated_at FROM middleware_config ORDER BY middleware_name """) return [ MiddlewareConfigResponse( id=str(row["id"]), middleware_name=row["middleware_name"], enabled=row["enabled"], config=row["config"] or {}, updated_at=row["updated_at"], ) for row in rows ] @router.get("/{name}", response_model=MiddlewareConfigResponse) async def get_middleware_config( name: str, session: Session = Depends(require_permission("settings:read")), ): """ Get configuration for a specific middleware. Requires: settings:read permission """ pool = await get_db_pool() row = await pool.fetchrow(""" SELECT id, middleware_name, enabled, config, updated_at FROM middleware_config WHERE middleware_name = $1 """, name) if not row: raise HTTPException(status_code=404, detail=f"Middleware '{name}' not found") return MiddlewareConfigResponse( id=str(row["id"]), middleware_name=row["middleware_name"], enabled=row["enabled"], config=row["config"] or {}, updated_at=row["updated_at"], ) @router.put("/{name}", response_model=MiddlewareConfigResponse) async def update_middleware_config( name: str, data: MiddlewareConfigUpdateRequest, session: Session = Depends(require_permission("settings:write")), ): """ Update configuration for a specific middleware. Requires: settings:write permission """ pool = await get_db_pool() # Build update query dynamically updates = [] params = [name] param_idx = 2 if data.enabled is not None: updates.append(f"enabled = ${param_idx}") params.append(data.enabled) param_idx += 1 if data.config is not None: updates.append(f"config = ${param_idx}") params.append(data.config) param_idx += 1 if not updates: raise HTTPException(status_code=400, detail="No fields to update") updates.append("updated_at = NOW()") updates.append(f"updated_by = ${param_idx}") params.append(session.user_id) query = f""" UPDATE middleware_config SET {", ".join(updates)} WHERE middleware_name = $1 RETURNING id, middleware_name, enabled, config, updated_at """ row = await pool.fetchrow(query, *params) if not row: raise HTTPException(status_code=404, detail=f"Middleware '{name}' not found") # Log the configuration change await pool.execute(""" INSERT INTO middleware_events (middleware_name, event_type, user_id, details) VALUES ($1, 'config_changed', $2, $3) """, name, session.user_id, {"changes": data.dict(exclude_none=True)}) return MiddlewareConfigResponse( id=str(row["id"]), middleware_name=row["middleware_name"], enabled=row["enabled"], config=row["config"] or {}, updated_at=row["updated_at"], ) # ============================================== # Rate Limiting IP Management # ============================================== @router.get("/rate-limit/ip-list", response_model=List[RateLimitIPResponse]) async def list_rate_limit_ips( list_type: Optional[str] = Query(None, pattern="^(whitelist|blacklist)$"), session: Session = Depends(require_permission("settings:read")), ): """ List all IPs in whitelist/blacklist. Requires: settings:read permission """ pool = await get_db_pool() if list_type: rows = await pool.fetch(""" SELECT id, ip_address::text, list_type, reason, expires_at, created_at FROM rate_limit_ip_list WHERE list_type = $1 ORDER BY created_at DESC """, list_type) else: rows = await pool.fetch(""" SELECT id, ip_address::text, list_type, reason, expires_at, created_at FROM rate_limit_ip_list ORDER BY list_type, created_at DESC """) return [ RateLimitIPResponse( id=str(row["id"]), ip_address=row["ip_address"], list_type=row["list_type"], reason=row["reason"], expires_at=row["expires_at"], created_at=row["created_at"], ) for row in rows ] @router.post("/rate-limit/ip-list", response_model=RateLimitIPResponse, status_code=201) async def add_rate_limit_ip( data: RateLimitIPRequest, session: Session = Depends(require_permission("settings:write")), ): """ Add IP to whitelist or blacklist. Requires: settings:write permission """ pool = await get_db_pool() try: row = await pool.fetchrow(""" INSERT INTO rate_limit_ip_list (ip_address, list_type, reason, expires_at, created_by) VALUES ($1::inet, $2, $3, $4, $5) RETURNING id, ip_address::text, list_type, reason, expires_at, created_at """, data.ip_address, data.list_type, data.reason, data.expires_at, session.user_id) except asyncpg.UniqueViolationError: raise HTTPException( status_code=409, detail=f"IP {data.ip_address} already exists in {data.list_type}" ) # Log the event await pool.execute(""" INSERT INTO middleware_events (middleware_name, event_type, ip_address, user_id, details) VALUES ('rate_limiter', $1, $2::inet, $3, $4) """, f"ip_{data.list_type}_add", data.ip_address, session.user_id, {"reason": data.reason}) return RateLimitIPResponse( id=str(row["id"]), ip_address=row["ip_address"], list_type=row["list_type"], reason=row["reason"], expires_at=row["expires_at"], created_at=row["created_at"], ) @router.delete("/rate-limit/ip-list/{ip_id}") async def remove_rate_limit_ip( ip_id: str, session: Session = Depends(require_permission("settings:write")), ): """ Remove IP from whitelist/blacklist. Requires: settings:write permission """ pool = await get_db_pool() # Get the entry first for logging row = await pool.fetchrow(""" SELECT ip_address::text, list_type FROM rate_limit_ip_list WHERE id = $1 """, ip_id) if not row: raise HTTPException(status_code=404, detail="IP entry not found") await pool.execute(""" DELETE FROM rate_limit_ip_list WHERE id = $1 """, ip_id) # Log the event await pool.execute(""" INSERT INTO middleware_events (middleware_name, event_type, ip_address, user_id, details) VALUES ('rate_limiter', $1, $2::inet, $3, $4) """, f"ip_{row['list_type']}_remove", row["ip_address"], session.user_id, {}) return {"message": "IP removed successfully"} # ============================================== # Middleware Events & Statistics # ============================================== @router.get("/events", response_model=List[MiddlewareEventResponse]) async def list_middleware_events( middleware_name: Optional[str] = None, event_type: Optional[str] = None, limit: int = Query(100, le=1000), offset: int = 0, session: Session = Depends(require_permission("audit:read")), ): """ List middleware events (rate limit triggers, config changes, etc.). Requires: audit:read permission """ pool = await get_db_pool() conditions = [] params = [] param_idx = 1 if middleware_name: conditions.append(f"middleware_name = ${param_idx}") params.append(middleware_name) param_idx += 1 if event_type: conditions.append(f"event_type = ${param_idx}") params.append(event_type) param_idx += 1 where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" params.extend([limit, offset]) query = f""" SELECT id, middleware_name, event_type, ip_address::text, user_id::text, request_path, request_method, details, created_at FROM middleware_events {where_clause} ORDER BY created_at DESC LIMIT ${param_idx} OFFSET ${param_idx + 1} """ rows = await pool.fetch(query, *params) return [ MiddlewareEventResponse( id=str(row["id"]), middleware_name=row["middleware_name"], event_type=row["event_type"], ip_address=row["ip_address"], user_id=row["user_id"], request_path=row["request_path"], request_method=row["request_method"], details=row["details"], created_at=row["created_at"], ) for row in rows ] @router.get("/stats", response_model=List[MiddlewareStatsResponse]) async def get_middleware_stats( session: Session = Depends(require_permission("settings:read")), ): """ Get statistics for all middlewares. Requires: settings:read permission """ pool = await get_db_pool() stats = [] middlewares = ["request_id", "security_headers", "cors", "rate_limiter", "pii_redactor", "input_gate"] for mw in middlewares: # Get event counts counts = await pool.fetchrow(""" SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as last_hour, COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours') as last_24h FROM middleware_events WHERE middleware_name = $1 """, mw) # Get top event types top_events = await pool.fetch(""" SELECT event_type, COUNT(*) as count FROM middleware_events WHERE middleware_name = $1 AND created_at > NOW() - INTERVAL '24 hours' GROUP BY event_type ORDER BY count DESC LIMIT 5 """, mw) # Get top IPs (for rate limiter) top_ips = await pool.fetch(""" SELECT ip_address::text, COUNT(*) as count FROM middleware_events WHERE middleware_name = $1 AND ip_address IS NOT NULL AND created_at > NOW() - INTERVAL '24 hours' GROUP BY ip_address ORDER BY count DESC LIMIT 5 """, mw) stats.append(MiddlewareStatsResponse( middleware_name=mw, total_events=counts["total"] or 0, events_last_hour=counts["last_hour"] or 0, events_last_24h=counts["last_24h"] or 0, top_event_types=[ {"event_type": r["event_type"], "count": r["count"]} for r in top_events ], top_ips=[ {"ip_address": r["ip_address"], "count": r["count"]} for r in top_ips ], )) return stats @router.get("/stats/{name}", response_model=MiddlewareStatsResponse) async def get_middleware_stats_by_name( name: str, session: Session = Depends(require_permission("settings:read")), ): """ Get statistics for a specific middleware. Requires: settings:read permission """ pool = await get_db_pool() # Get event counts counts = await pool.fetchrow(""" SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as last_hour, COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours') as last_24h FROM middleware_events WHERE middleware_name = $1 """, name) # Get top event types top_events = await pool.fetch(""" SELECT event_type, COUNT(*) as count FROM middleware_events WHERE middleware_name = $1 AND created_at > NOW() - INTERVAL '24 hours' GROUP BY event_type ORDER BY count DESC LIMIT 10 """, name) # Get top IPs top_ips = await pool.fetch(""" SELECT ip_address::text, COUNT(*) as count FROM middleware_events WHERE middleware_name = $1 AND ip_address IS NOT NULL AND created_at > NOW() - INTERVAL '24 hours' GROUP BY ip_address ORDER BY count DESC LIMIT 10 """, name) return MiddlewareStatsResponse( middleware_name=name, total_events=counts["total"] or 0, events_last_hour=counts["last_hour"] or 0, events_last_24h=counts["last_24h"] or 0, top_event_types=[ {"event_type": r["event_type"], "count": r["count"]} for r in top_events ], top_ips=[ {"ip_address": r["ip_address"], "count": r["count"]} for r in top_ips ], )