security: Sprint 00 — critical security fixes (OPC WS auth, Transmission creds, SECRET_KEY, passkey rate limit)
- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking) - Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars - Remove hardcoded SECRET_KEY fallback from dev compose - Rate-limit passkey register options endpoint at 5/minute - Add PDD (docs/improvement-plan.md) and sprint specs (specs/) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,15 +6,14 @@ import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from fastapi import APIRouter, HTTPException, Query, WebSocket, WebSocketDisconnect, status
|
||||
|
||||
import auth_service as auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Active WebSocket connections
|
||||
active_connections: set[WebSocket] = set()
|
||||
|
||||
|
||||
def serialize_datetime(obj: Any) -> Any:
|
||||
"""Recursively serialize datetime objects to ISO format strings"""
|
||||
@@ -29,21 +28,21 @@ def serialize_datetime(obj: Any) -> Any:
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manages WebSocket connections"""
|
||||
"""Manages WebSocket connections with per-user tracking"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: set[WebSocket] = set()
|
||||
self.active_connections: dict[WebSocket, str] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
async def connect(self, websocket: WebSocket, username: str):
|
||||
"""Accept and register a new connection"""
|
||||
await websocket.accept()
|
||||
self.active_connections.add(websocket)
|
||||
logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
self.active_connections[websocket] = username
|
||||
logger.info(f"WebSocket connected: {username}. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
"""Remove a connection"""
|
||||
self.active_connections.discard(websocket)
|
||||
logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
username = self.active_connections.pop(websocket, "unknown")
|
||||
logger.info(f"WebSocket disconnected: {username}. Total connections: {len(self.active_connections)}")
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
"""Broadcast message to all connected clients"""
|
||||
@@ -64,9 +63,19 @@ manager = ConnectionManager()
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for OPC real-time updates"""
|
||||
await manager.connect(websocket)
|
||||
async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)):
|
||||
"""WebSocket endpoint for OPC real-time updates. Requires JWT token via ?token= query param."""
|
||||
if not token:
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Missing authentication token")
|
||||
return
|
||||
|
||||
try:
|
||||
user = auth.user_from_access_token(token, include_auth_header=False)
|
||||
except HTTPException:
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Invalid authentication token")
|
||||
return
|
||||
|
||||
await manager.connect(websocket, user.username)
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
||||
Reference in New Issue
Block a user