147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
"""
|
|
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/.*/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)
|