Improve GDPR implementation
This commit is contained in:
@@ -21,6 +21,7 @@ from app.middleware.logging import LoggingMiddleware
|
||||
from app.middleware.rate_limit import RateLimitMiddleware
|
||||
from app.middleware.subscription import SubscriptionMiddleware
|
||||
from app.middleware.demo_middleware import DemoMiddleware
|
||||
from app.middleware.read_only_mode import ReadOnlyModeMiddleware
|
||||
from app.routes import auth, tenant, notification, nominatim, user, subscription, demo, pos
|
||||
from shared.monitoring.logging import setup_logging
|
||||
from shared.monitoring.metrics import MetricsCollector
|
||||
@@ -54,10 +55,11 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
# Custom middleware - Add in REVERSE order (last added = first executed)
|
||||
# Execution order: RequestIDMiddleware -> DemoMiddleware -> AuthMiddleware -> SubscriptionMiddleware -> RateLimitMiddleware -> LoggingMiddleware
|
||||
app.add_middleware(LoggingMiddleware) # Executes 6th (outermost)
|
||||
app.add_middleware(RateLimitMiddleware, calls_per_minute=300) # Executes 5th
|
||||
app.add_middleware(SubscriptionMiddleware, tenant_service_url=settings.TENANT_SERVICE_URL) # Executes 4th
|
||||
# Execution order: RequestIDMiddleware -> DemoMiddleware -> AuthMiddleware -> ReadOnlyModeMiddleware -> SubscriptionMiddleware -> RateLimitMiddleware -> LoggingMiddleware
|
||||
app.add_middleware(LoggingMiddleware) # Executes 7th (outermost)
|
||||
app.add_middleware(RateLimitMiddleware, calls_per_minute=300) # Executes 6th
|
||||
app.add_middleware(SubscriptionMiddleware, tenant_service_url=settings.TENANT_SERVICE_URL) # Executes 5th
|
||||
app.add_middleware(ReadOnlyModeMiddleware, tenant_service_url=settings.TENANT_SERVICE_URL) # Executes 4th - Enforce read-only mode
|
||||
app.add_middleware(AuthMiddleware) # Executes 3rd - Checks for demo context
|
||||
app.add_middleware(DemoMiddleware) # Executes 2nd - Sets demo user context
|
||||
app.add_middleware(RequestIDMiddleware) # Executes 1st (innermost) - Generates request ID for tracing
|
||||
|
||||
140
gateway/app/middleware/read_only_mode.py
Normal file
140
gateway/app/middleware/read_only_mode.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user