""" Gateway middleware to enforce read-only mode for subscriptions with status: - pending_cancellation (until cancellation_effective_date) - inactive (after cancellation or no active subscription) Allowed operations in read-only mode: - GET requests (all read operations) - POST /api/v1/users/me/delete/request (account deletion) - POST /api/v1/subscriptions/reactivate (subscription reactivation) - POST /api/v1/subscriptions/* (subscription management) """ import httpx import logging from fastapi import Request, HTTPException, status from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from typing import Optional import re logger = logging.getLogger(__name__) # Whitelist of POST/PUT/DELETE endpoints allowed in read-only mode READ_ONLY_WHITELIST_PATTERNS = [ r'^/api/v1/users/me/delete/request$', r'^/api/v1/users/me/export.*$', r'^/api/v1/subscriptions/.*', r'^/api/v1/auth/.*', # Allow auth operations r'^/api/v1/tenants/register$', # Allow new tenant registration (no existing tenant context) r'^/api/v1/tenants/.*/orchestrator/run-daily-workflow$', # Allow workflow testing r'^/api/v1/tenants/.*/inventory/ml/insights/.*', # Allow ML insights (safety stock optimization) r'^/api/v1/tenants/.*/production/ml/insights/.*', # Allow ML insights (yield prediction) r'^/api/v1/tenants/.*/procurement/ml/insights/.*', # Allow ML insights (supplier analysis, price forecasting) r'^/api/v1/tenants/.*/forecasting/ml/insights/.*', # Allow ML insights (rules generation) r'^/api/v1/tenants/.*/forecasting/operations/.*', # Allow forecasting operations ] class ReadOnlyModeMiddleware(BaseHTTPMiddleware): """ Middleware to enforce read-only mode based on subscription status """ def __init__(self, app, tenant_service_url: str = "http://tenant-service:8000"): super().__init__(app) self.tenant_service_url = tenant_service_url self.cache = {} self.cache_ttl = 60 async def check_subscription_status(self, tenant_id: str, authorization: str) -> dict: """ Check subscription status from tenant service Returns subscription data including status and read_only flag """ try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get( f"{self.tenant_service_url}/api/v1/subscriptions/{tenant_id}/status", headers={"Authorization": authorization} ) if response.status_code == 200: return response.json() elif response.status_code == 404: return {"status": "inactive", "is_read_only": True} else: logger.warning( f"Failed to check subscription status: {response.status_code}", extra={"tenant_id": tenant_id} ) return {"status": "unknown", "is_read_only": False} except Exception as e: logger.error( f"Error checking subscription status: {e}", extra={"tenant_id": tenant_id} ) return {"status": "unknown", "is_read_only": False} def is_whitelisted_endpoint(self, path: str) -> bool: """ Check if endpoint is whitelisted for read-only mode """ for pattern in READ_ONLY_WHITELIST_PATTERNS: if re.match(pattern, path): return True return False def is_write_operation(self, method: str) -> bool: """ Determine if HTTP method is a write operation """ return method.upper() in ['POST', 'PUT', 'DELETE', 'PATCH'] async def dispatch(self, request: Request, call_next): """ Process each request through read-only mode check """ tenant_id = request.headers.get("X-Tenant-ID") authorization = request.headers.get("Authorization") path = request.url.path method = request.method if not tenant_id or not authorization: return await call_next(request) if method.upper() == 'GET': return await call_next(request) if self.is_whitelisted_endpoint(path): return await call_next(request) if self.is_write_operation(method): subscription_data = await self.check_subscription_status(tenant_id, authorization) if subscription_data.get("is_read_only", False): status_detail = subscription_data.get("status", "inactive") effective_date = subscription_data.get("cancellation_effective_date") error_message = { "detail": "Account is in read-only mode", "reason": f"Subscription status: {status_detail}", "message": "Your subscription has been cancelled. You can view data but cannot make changes.", "action_required": "Reactivate your subscription to regain full access", "reactivation_url": "/app/settings/subscription" } if effective_date: error_message["read_only_until"] = effective_date error_message["message"] = f"Your subscription is pending cancellation. Read-only mode starts on {effective_date}." logger.info( "read_only_mode_enforced", extra={ "tenant_id": tenant_id, "path": path, "method": method, "subscription_status": status_detail } ) return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, content=error_message ) return await call_next(request)