Production code:
- pose_service.py: real uptime tracking (_start_time), real calibration
state machine (_calibration_in_progress, _calibration_id), proper
get_calibration_status() using elapsed time, uptime in health_check()
- health.py: _APP_START_TIME module constant for real uptime_seconds
- dependencies.py: remove TODO, document JWT config requirement clearly
ADR-017 status: Proposed → Accepted (all 7 integrations complete)
Test fixes (170 unit tests — 0 failures):
- Fix hardcoded /workspaces/wifi-densepose devcontainer paths in 4 files;
replaced with os.path relative to __file__
- test_csi_extractor_tdd/standalone: update ESP32 fixture to provide
correct 3×56 amplitude+phase values (was only 3 values)
- test_csi_standalone/tdd_complete: Atheros tests now expect
CSIExtractionError (implementation raises it correctly)
- test_router_interface_tdd: register module in sys.modules so
patch('src.hardware.router_interface...') resolves; fix
test_should_parse_csi_response to expect RouterConnectionError
- test_csi_processor: rewrite to use actual preprocess_csi_data /
extract_features API with proper CSIData fixtures; fix constructor
- test_phase_sanitizer: fix constructor (requires config), rename
sanitize() → sanitize_phase(), fix empty-data fixture (use 2D array),
fix phase data to stay within [-π, π] validation range
Proof bundle: PASS — SHA-256 hash matches, no random patterns in prod code
https://claude.ai/code/session_01BSBAQJ34SLkiJy4A8SoiL4
467 lines
14 KiB
Python
467 lines
14 KiB
Python
"""
|
|
Dependency injection for WiFi-DensePose API
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
from functools import lru_cache
|
|
|
|
from fastapi import Depends, HTTPException, status, Request
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
|
from src.config.settings import get_settings
|
|
from src.config.domains import get_domain_config
|
|
from src.services.pose_service import PoseService
|
|
from src.services.stream_service import StreamService
|
|
from src.services.hardware_service import HardwareService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Security scheme for JWT authentication
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
|
|
# Service dependencies
|
|
@lru_cache()
|
|
def get_pose_service() -> PoseService:
|
|
"""Get pose service instance."""
|
|
settings = get_settings()
|
|
domain_config = get_domain_config()
|
|
|
|
return PoseService(
|
|
settings=settings,
|
|
domain_config=domain_config
|
|
)
|
|
|
|
|
|
@lru_cache()
|
|
def get_stream_service() -> StreamService:
|
|
"""Get stream service instance."""
|
|
settings = get_settings()
|
|
domain_config = get_domain_config()
|
|
|
|
return StreamService(
|
|
settings=settings,
|
|
domain_config=domain_config
|
|
)
|
|
|
|
|
|
@lru_cache()
|
|
def get_hardware_service() -> HardwareService:
|
|
"""Get hardware service instance."""
|
|
settings = get_settings()
|
|
domain_config = get_domain_config()
|
|
|
|
return HardwareService(
|
|
settings=settings,
|
|
domain_config=domain_config
|
|
)
|
|
|
|
|
|
# Authentication dependencies
|
|
async def get_current_user(
|
|
request: Request,
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Get current authenticated user."""
|
|
settings = get_settings()
|
|
|
|
# Skip authentication if disabled
|
|
if not settings.enable_authentication:
|
|
return None
|
|
|
|
# Check if user is already set by middleware
|
|
if hasattr(request.state, 'user') and request.state.user:
|
|
return request.state.user
|
|
|
|
# No credentials provided
|
|
if not credentials:
|
|
return None
|
|
|
|
# Validate the JWT token
|
|
# JWT validation must be configured via settings (e.g. JWT_SECRET, JWT_ALGORITHM)
|
|
if settings.is_development:
|
|
logger.warning(
|
|
"Authentication credentials provided in development mode but JWT "
|
|
"validation is not configured. Set up JWT authentication via "
|
|
"environment variables (JWT_SECRET, JWT_ALGORITHM) or disable "
|
|
"authentication. Rejecting request."
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=(
|
|
"JWT authentication is not configured. In development mode, either "
|
|
"disable authentication (enable_authentication=False) or configure "
|
|
"JWT validation. Returning mock users is not permitted in any environment."
|
|
),
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# In production, implement proper JWT validation
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=(
|
|
"JWT authentication is not configured. Configure JWT_SECRET and "
|
|
"JWT_ALGORITHM environment variables, or integrate an external "
|
|
"identity provider. See docs/authentication.md for setup instructions."
|
|
),
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
async def get_current_active_user(
|
|
current_user: Optional[Dict[str, Any]] = Depends(get_current_user)
|
|
) -> Dict[str, Any]:
|
|
"""Get current active user (required authentication)."""
|
|
if not current_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# Check if user is active
|
|
if not current_user.get("is_active", True):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Inactive user"
|
|
)
|
|
|
|
return current_user
|
|
|
|
|
|
async def get_admin_user(
|
|
current_user: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
"""Get current admin user (admin privileges required)."""
|
|
if not current_user.get("is_admin", False):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin privileges required"
|
|
)
|
|
|
|
return current_user
|
|
|
|
|
|
# Permission dependencies
|
|
def require_permission(permission: str):
|
|
"""Dependency factory for permission checking."""
|
|
|
|
async def check_permission(
|
|
current_user: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
"""Check if user has required permission."""
|
|
user_permissions = current_user.get("permissions", [])
|
|
|
|
# Admin users have all permissions
|
|
if current_user.get("is_admin", False):
|
|
return current_user
|
|
|
|
# Check specific permission
|
|
if permission not in user_permissions:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Permission '{permission}' required"
|
|
)
|
|
|
|
return current_user
|
|
|
|
return check_permission
|
|
|
|
|
|
# Zone access dependencies
|
|
async def validate_zone_access(
|
|
zone_id: str,
|
|
current_user: Optional[Dict[str, Any]] = Depends(get_current_user)
|
|
) -> str:
|
|
"""Validate user access to a specific zone."""
|
|
domain_config = get_domain_config()
|
|
|
|
# Check if zone exists
|
|
zone = domain_config.get_zone(zone_id)
|
|
if not zone:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Zone '{zone_id}' not found"
|
|
)
|
|
|
|
# Check if zone is enabled
|
|
if not zone.enabled:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Zone '{zone_id}' is disabled"
|
|
)
|
|
|
|
# If authentication is enabled, check user access
|
|
if current_user:
|
|
# Admin users have access to all zones
|
|
if current_user.get("is_admin", False):
|
|
return zone_id
|
|
|
|
# Check user's zone permissions
|
|
user_zones = current_user.get("zones", [])
|
|
if user_zones and zone_id not in user_zones:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Access denied to zone '{zone_id}'"
|
|
)
|
|
|
|
return zone_id
|
|
|
|
|
|
# Router access dependencies
|
|
async def validate_router_access(
|
|
router_id: str,
|
|
current_user: Optional[Dict[str, Any]] = Depends(get_current_user)
|
|
) -> str:
|
|
"""Validate user access to a specific router."""
|
|
domain_config = get_domain_config()
|
|
|
|
# Check if router exists
|
|
router = domain_config.get_router(router_id)
|
|
if not router:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Router '{router_id}' not found"
|
|
)
|
|
|
|
# Check if router is enabled
|
|
if not router.enabled:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Router '{router_id}' is disabled"
|
|
)
|
|
|
|
# If authentication is enabled, check user access
|
|
if current_user:
|
|
# Admin users have access to all routers
|
|
if current_user.get("is_admin", False):
|
|
return router_id
|
|
|
|
# Check user's router permissions
|
|
user_routers = current_user.get("routers", [])
|
|
if user_routers and router_id not in user_routers:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Access denied to router '{router_id}'"
|
|
)
|
|
|
|
return router_id
|
|
|
|
|
|
# Service health dependencies
|
|
async def check_service_health(
|
|
request: Request,
|
|
service_name: str
|
|
) -> bool:
|
|
"""Check if a service is healthy."""
|
|
try:
|
|
if service_name == "pose":
|
|
service = getattr(request.app.state, 'pose_service', None)
|
|
elif service_name == "stream":
|
|
service = getattr(request.app.state, 'stream_service', None)
|
|
elif service_name == "hardware":
|
|
service = getattr(request.app.state, 'hardware_service', None)
|
|
else:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Unknown service: {service_name}"
|
|
)
|
|
|
|
if not service:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"Service '{service_name}' not available"
|
|
)
|
|
|
|
# Check service health
|
|
status_info = await service.get_status()
|
|
if status_info.get("status") != "healthy":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"Service '{service_name}' is unhealthy: {status_info.get('error', 'Unknown error')}"
|
|
)
|
|
|
|
return True
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error checking service health for {service_name}: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=f"Service '{service_name}' health check failed"
|
|
)
|
|
|
|
|
|
# Rate limiting dependencies
|
|
async def check_rate_limit(
|
|
request: Request,
|
|
current_user: Optional[Dict[str, Any]] = Depends(get_current_user)
|
|
) -> bool:
|
|
"""Check rate limiting status."""
|
|
settings = get_settings()
|
|
|
|
# Skip if rate limiting is disabled
|
|
if not settings.enable_rate_limiting:
|
|
return True
|
|
|
|
# Rate limiting is handled by middleware
|
|
# This dependency can be used for additional checks
|
|
return True
|
|
|
|
|
|
# Configuration dependencies
|
|
def get_zone_config(zone_id: str = Depends(validate_zone_access)):
|
|
"""Get zone configuration."""
|
|
domain_config = get_domain_config()
|
|
return domain_config.get_zone(zone_id)
|
|
|
|
|
|
def get_router_config(router_id: str = Depends(validate_router_access)):
|
|
"""Get router configuration."""
|
|
domain_config = get_domain_config()
|
|
return domain_config.get_router(router_id)
|
|
|
|
|
|
# Pagination dependencies
|
|
class PaginationParams:
|
|
"""Pagination parameters."""
|
|
|
|
def __init__(
|
|
self,
|
|
page: int = 1,
|
|
size: int = 20,
|
|
max_size: int = 100
|
|
):
|
|
if page < 1:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Page must be >= 1"
|
|
)
|
|
|
|
if size < 1:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Size must be >= 1"
|
|
)
|
|
|
|
if size > max_size:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Size must be <= {max_size}"
|
|
)
|
|
|
|
self.page = page
|
|
self.size = size
|
|
self.offset = (page - 1) * size
|
|
self.limit = size
|
|
|
|
|
|
def get_pagination_params(
|
|
page: int = 1,
|
|
size: int = 20
|
|
) -> PaginationParams:
|
|
"""Get pagination parameters."""
|
|
return PaginationParams(page=page, size=size)
|
|
|
|
|
|
# Query filter dependencies
|
|
class QueryFilters:
|
|
"""Common query filters."""
|
|
|
|
def __init__(
|
|
self,
|
|
start_time: Optional[str] = None,
|
|
end_time: Optional[str] = None,
|
|
min_confidence: Optional[float] = None,
|
|
activity: Optional[str] = None
|
|
):
|
|
self.start_time = start_time
|
|
self.end_time = end_time
|
|
self.min_confidence = min_confidence
|
|
self.activity = activity
|
|
|
|
# Validate confidence
|
|
if min_confidence is not None:
|
|
if not 0.0 <= min_confidence <= 1.0:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="min_confidence must be between 0.0 and 1.0"
|
|
)
|
|
|
|
|
|
def get_query_filters(
|
|
start_time: Optional[str] = None,
|
|
end_time: Optional[str] = None,
|
|
min_confidence: Optional[float] = None,
|
|
activity: Optional[str] = None
|
|
) -> QueryFilters:
|
|
"""Get query filters."""
|
|
return QueryFilters(
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
min_confidence=min_confidence,
|
|
activity=activity
|
|
)
|
|
|
|
|
|
# WebSocket dependencies
|
|
async def get_websocket_user(
|
|
websocket_token: Optional[str] = None
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Get user from WebSocket token."""
|
|
settings = get_settings()
|
|
|
|
# Skip authentication if disabled
|
|
if not settings.enable_authentication:
|
|
return None
|
|
|
|
# Validate the WebSocket token
|
|
if not websocket_token:
|
|
return None
|
|
|
|
if settings.is_development:
|
|
logger.warning(
|
|
"WebSocket token provided in development mode but token validation "
|
|
"is not configured. Rejecting. Disable authentication or configure "
|
|
"JWT validation to allow WebSocket connections."
|
|
)
|
|
return None
|
|
|
|
# WebSocket token validation requires a configured JWT secret and issuer.
|
|
# Until JWT settings are provided via environment variables
|
|
# (JWT_SECRET_KEY, JWT_ALGORITHM), tokens are rejected to prevent
|
|
# unauthorised access. Configure authentication settings and implement
|
|
# token verification here using the same logic as get_current_user().
|
|
logger.warning("WebSocket token validation requires JWT configuration. Rejecting token.")
|
|
return None
|
|
|
|
|
|
async def get_current_user_ws(
|
|
websocket_token: Optional[str] = None
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""Get current user for WebSocket connections."""
|
|
return await get_websocket_user(websocket_token)
|
|
|
|
|
|
# Authentication requirement dependencies
|
|
async def require_auth(
|
|
current_user: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
"""Require authentication for endpoint access."""
|
|
return current_user
|
|
|
|
|
|
# Development dependencies
|
|
async def development_only():
|
|
"""Dependency that only allows access in development."""
|
|
settings = get_settings()
|
|
|
|
if not settings.is_development:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Endpoint not available in production"
|
|
)
|
|
|
|
return True |