Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
603 lines
20 KiB
Python
603 lines
20 KiB
Python
"""
|
|
Multi-tenant middleware for Django applications.
|
|
|
|
Provides tenant isolation and context management for all requests.
|
|
Supports multiple tenant identification methods and security features.
|
|
"""
|
|
|
|
import logging
|
|
from django.db import connection
|
|
from django.http import Http404, HttpResponseForbidden
|
|
from django.conf import settings
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
from django.contrib.auth.middleware import get_user
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.urls import resolve
|
|
from django.utils import timezone
|
|
from django.db import transaction
|
|
import re
|
|
import uuid
|
|
|
|
from ..models.tenant import Tenant
|
|
from ..models.user import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TenantMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware to identify and isolate tenants for each request.
|
|
"""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.tenant_model = Tenant
|
|
self.user_model = User
|
|
|
|
# Configure tenant identification methods
|
|
self.host_based_domains = getattr(settings, 'TENANT_HOST_BASED_DOMAINS', True)
|
|
self.path_based_prefix = getattr(settings, 'TENANT_PATH_BASED_PREFIX', '/tenant/')
|
|
self.header_based = getattr(settings, 'TENANT_HEADER_BASED', True)
|
|
self.tenant_header = getattr(settings, 'TENANT_HEADER_NAME', 'X-Tenant-ID')
|
|
|
|
# Security settings
|
|
self.enforce_tenant_isolation = getattr(settings, 'ENFORCE_TENANT_ISOLATION', True)
|
|
self.public_paths = getattr(settings, 'PUBLIC_PATHS', [
|
|
'/api/v1/auth/',
|
|
'/api/v1/public/',
|
|
'/health/',
|
|
'/metrics/',
|
|
'/api/v1/tenants/register',
|
|
'/admin/login/',
|
|
'/static/',
|
|
'/media/',
|
|
])
|
|
|
|
# Cache settings
|
|
self.cache_enabled = getattr(settings, 'TENANT_CACHE_ENABLED', True)
|
|
self.cache_timeout = getattr(settings, 'TENANT_CACHE_TIMEOUT', 300) # 5 minutes
|
|
|
|
# Admin paths that bypass tenant isolation
|
|
self.admin_paths = getattr(settings, 'ADMIN_PATHS', [
|
|
'/admin/',
|
|
'/api/v1/admin/',
|
|
])
|
|
|
|
# API prefix that requires tenant context
|
|
self.api_prefix = getattr(settings, 'API_PREFIX', '/api/v1/')
|
|
|
|
# Initialize regex patterns
|
|
self.public_path_patterns = [re.compile(path) for path in self.public_paths]
|
|
self.admin_path_patterns = [re.compile(path) for path in self.admin_paths]
|
|
|
|
def process_request(self, request):
|
|
"""
|
|
Process incoming request to identify tenant.
|
|
"""
|
|
request.tenant = None
|
|
request.tenant_id = None
|
|
request.tenant_context = {}
|
|
request.is_tenant_request = False
|
|
|
|
# Skip tenant processing for public paths
|
|
if self._is_public_path(request.path):
|
|
logger.debug(f"Skipping tenant processing for public path: {request.path}")
|
|
return None
|
|
|
|
# Skip tenant processing for admin paths (handled by authentication)
|
|
if self._is_admin_path(request.path):
|
|
logger.debug(f"Skipping tenant processing for admin path: {request.path}")
|
|
return None
|
|
|
|
# Try to identify tenant using various methods
|
|
tenant = self._identify_tenant(request)
|
|
|
|
if not tenant:
|
|
# For API paths that require tenant context, return 404
|
|
if request.path.startswith(self.api_prefix):
|
|
logger.warning(f"Tenant not found for API request: {request.path}")
|
|
raise Http404("Tenant not found")
|
|
|
|
# For other paths, allow but without tenant context
|
|
return None
|
|
|
|
# Set tenant context
|
|
request.tenant = tenant
|
|
request.tenant_id = tenant.id
|
|
request.tenant_context = self._build_tenant_context(tenant)
|
|
request.is_tenant_request = True
|
|
|
|
# Set database connection tenant context
|
|
self._set_database_tenant_context(tenant)
|
|
|
|
# Set cache tenant context
|
|
if self.cache_enabled:
|
|
self._set_cache_tenant_context(tenant)
|
|
|
|
# Validate tenant status
|
|
if not self._validate_tenant_status(tenant):
|
|
logger.warning(f"Tenant {tenant.id} is not active: {tenant.status}")
|
|
return HttpResponseForbidden("Tenant account is not active")
|
|
|
|
# Validate user access to tenant if authenticated
|
|
user = get_user(request)
|
|
if user.is_authenticated and not user.is_superuser:
|
|
if not self._validate_user_tenant_access(user, tenant):
|
|
logger.warning(f"User {user.id} attempted to access tenant {tenant.id}")
|
|
return HttpResponseForbidden("Access denied to this tenant")
|
|
|
|
logger.debug(f"Tenant context set: {tenant.name} ({tenant.id})")
|
|
return None
|
|
|
|
def process_response(self, request, response):
|
|
"""
|
|
Clean up tenant context after request processing.
|
|
"""
|
|
# Clear database tenant context
|
|
self._clear_database_tenant_context()
|
|
|
|
# Clear cache tenant context
|
|
if self.cache_enabled:
|
|
self._clear_cache_tenant_context()
|
|
|
|
return response
|
|
|
|
def process_exception(self, request, exception):
|
|
"""
|
|
Handle exceptions and clean up tenant context.
|
|
"""
|
|
# Clear database tenant context
|
|
self._clear_database_tenant_context()
|
|
|
|
# Clear cache tenant context
|
|
if self.cache_enabled:
|
|
self._clear_cache_tenant_context()
|
|
|
|
return None
|
|
|
|
def _is_public_path(self, path):
|
|
"""
|
|
Check if path is public and doesn't require tenant context.
|
|
"""
|
|
return any(pattern.match(path) for pattern in self.public_path_patterns)
|
|
|
|
def _is_admin_path(self, path):
|
|
"""
|
|
Check if path is admin path.
|
|
"""
|
|
return any(pattern.match(path) for pattern in self.admin_path_patterns)
|
|
|
|
def _identify_tenant(self, request):
|
|
"""
|
|
Identify tenant using various methods.
|
|
"""
|
|
tenant = None
|
|
|
|
# Method 1: Host-based tenant identification
|
|
if self.host_based_domains and not tenant:
|
|
tenant = self._identify_tenant_by_host(request)
|
|
|
|
# Method 2: Path-based tenant identification
|
|
if not tenant:
|
|
tenant = self._identify_tenant_by_path(request)
|
|
|
|
# Method 3: Header-based tenant identification
|
|
if self.header_based and not tenant:
|
|
tenant = self._identify_tenant_by_header(request)
|
|
|
|
# Method 4: Subdomain-based tenant identification
|
|
if not tenant:
|
|
tenant = self._identify_tenant_by_subdomain(request)
|
|
|
|
# Method 5: User-based tenant identification (if authenticated)
|
|
if not tenant:
|
|
tenant = self._identify_tenant_by_user(request)
|
|
|
|
return tenant
|
|
|
|
def _identify_tenant_by_host(self, request):
|
|
"""
|
|
Identify tenant by hostname.
|
|
"""
|
|
host = request.get_host().split(':')[0] # Remove port
|
|
|
|
# Try to find tenant by custom domain
|
|
tenant = self._get_tenant_from_cache(f'tenant:host:{host}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
domain_mappings__contains=[host],
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:host:{host}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by host {host}: {e}")
|
|
|
|
return tenant
|
|
|
|
def _identify_tenant_by_path(self, request):
|
|
"""
|
|
Identify tenant by URL path.
|
|
"""
|
|
if request.path.startswith(self.path_based_prefix):
|
|
tenant_slug = request.path[len(self.path_based_prefix):].split('/')[0]
|
|
if tenant_slug:
|
|
tenant = self._get_tenant_from_cache(f'tenant:slug:{tenant_slug}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
slug=tenant_slug,
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:slug:{tenant_slug}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by slug {tenant_slug}: {e}")
|
|
|
|
return tenant
|
|
|
|
return None
|
|
|
|
def _identify_tenant_by_header(self, request):
|
|
"""
|
|
Identify tenant by request header.
|
|
"""
|
|
tenant_id = request.headers.get(self.tenant_header) or request.headers.get('X-Tenant-ID')
|
|
|
|
if tenant_id:
|
|
# Try to parse as UUID
|
|
try:
|
|
tenant_uuid = uuid.UUID(tenant_id)
|
|
tenant = self._get_tenant_from_cache(f'tenant:id:{tenant_uuid}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
id=tenant_uuid,
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:id:{tenant_uuid}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by ID {tenant_id}: {e}")
|
|
|
|
return tenant
|
|
except ValueError:
|
|
# Not a UUID, try as slug
|
|
tenant = self._get_tenant_from_cache(f'tenant:slug:{tenant_id}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
slug=tenant_id,
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:slug:{tenant_id}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by header {tenant_id}: {e}")
|
|
|
|
return tenant
|
|
|
|
return None
|
|
|
|
def _identify_tenant_by_subdomain(self, request):
|
|
"""
|
|
Identify tenant by subdomain.
|
|
"""
|
|
host = request.get_host().split(':')[0]
|
|
|
|
# Extract subdomain
|
|
parts = host.split('.')
|
|
if len(parts) > 2:
|
|
subdomain = parts[0]
|
|
|
|
tenant = self._get_tenant_from_cache(f'tenant:subdomain:{subdomain}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
slug=subdomain,
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:subdomain:{subdomain}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by subdomain {subdomain}: {e}")
|
|
|
|
return tenant
|
|
|
|
return None
|
|
|
|
def _identify_tenant_by_user(self, request):
|
|
"""
|
|
Identify tenant by authenticated user.
|
|
"""
|
|
user = get_user(request)
|
|
if user.is_authenticated and hasattr(user, 'tenant_id'):
|
|
tenant = self._get_tenant_from_cache(f'tenant:id:{user.tenant_id}')
|
|
if not tenant:
|
|
try:
|
|
tenant = self.tenant_model.objects.filter(
|
|
id=user.tenant_id,
|
|
status__in=['ACTIVE', 'PENDING']
|
|
).first()
|
|
|
|
if tenant:
|
|
self._set_tenant_cache(f'tenant:id:{user.tenant_id}', tenant)
|
|
except Exception as e:
|
|
logger.error(f"Error identifying tenant by user {user.id}: {e}")
|
|
|
|
return tenant
|
|
|
|
return None
|
|
|
|
def _build_tenant_context(self, tenant):
|
|
"""
|
|
Build tenant context dictionary.
|
|
"""
|
|
return {
|
|
'tenant_id': tenant.id,
|
|
'tenant_name': tenant.name,
|
|
'tenant_slug': tenant.slug,
|
|
'tenant_status': tenant.status,
|
|
'tenant_plan': tenant.subscription_plan,
|
|
'tenant_industry': tenant.business_type,
|
|
'tenant_timezone': tenant.timezone,
|
|
'tenant_currency': tenant.currency,
|
|
'tenant_locale': tenant.locale,
|
|
'tenant_features': tenant.get_features(),
|
|
'tenant_limits': {
|
|
'users': tenant.get_user_limits(),
|
|
'modules': tenant.get_module_limits(),
|
|
},
|
|
'is_trial': tenant.is_on_trial,
|
|
'trial_ends_at': tenant.trial_ends_at.isoformat() if tenant.trial_ends_at else None,
|
|
'subscription_active': tenant.subscription_active,
|
|
'subscription_ends_at': tenant.subscription_ends_at.isoformat() if tenant.subscription_ends_at else None,
|
|
}
|
|
|
|
def _validate_tenant_status(self, tenant):
|
|
"""
|
|
Validate that tenant is in acceptable state.
|
|
"""
|
|
if tenant.status == 'SUSPENDED':
|
|
return False
|
|
elif tenant.status == 'TERMINATED':
|
|
return False
|
|
elif tenant.status == 'PENDING' and not tenant.is_on_trial:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _validate_user_tenant_access(self, user, tenant):
|
|
"""
|
|
Validate that user has access to the specified tenant.
|
|
"""
|
|
# Superusers have access to all tenants
|
|
if user.is_superuser:
|
|
return True
|
|
|
|
# Check if user belongs to tenant
|
|
if user.tenant_id == tenant.id:
|
|
return True
|
|
|
|
# Check if user is staff with admin access
|
|
if user.is_staff and user.role in ['ADMIN', 'MANAGER']:
|
|
return True
|
|
|
|
return False
|
|
|
|
def _set_database_tenant_context(self, tenant):
|
|
"""
|
|
Set tenant context for database connection.
|
|
"""
|
|
if hasattr(connection, 'set_tenant'):
|
|
connection.set_tenant(tenant)
|
|
|
|
# Set session variable for PostgreSQL RLS
|
|
if connection.vendor == 'postgresql':
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SET app.current_tenant_id = %s", [str(tenant.id)])
|
|
|
|
def _clear_database_tenant_context(self):
|
|
"""
|
|
Clear tenant context from database connection.
|
|
"""
|
|
if hasattr(connection, 'clear_tenant'):
|
|
connection.clear_tenant()
|
|
|
|
# Clear session variable for PostgreSQL RLS
|
|
if connection.vendor == 'postgresql':
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("RESET app.current_tenant_id")
|
|
|
|
def _set_cache_tenant_context(self, tenant):
|
|
"""
|
|
Set tenant context for cache keys.
|
|
"""
|
|
if hasattr(cache, 'set_tenant'):
|
|
cache.set_tenant(tenant)
|
|
|
|
def _clear_cache_tenant_context(self):
|
|
"""
|
|
Clear tenant context from cache.
|
|
"""
|
|
if hasattr(cache, 'clear_tenant'):
|
|
cache.clear_tenant()
|
|
|
|
def _get_tenant_from_cache(self, cache_key):
|
|
"""
|
|
Get tenant from cache.
|
|
"""
|
|
if not self.cache_enabled:
|
|
return None
|
|
|
|
try:
|
|
return cache.get(cache_key)
|
|
except Exception as e:
|
|
logger.error(f"Error getting tenant from cache {cache_key}: {e}")
|
|
return None
|
|
|
|
def _set_tenant_cache(self, cache_key, tenant):
|
|
"""
|
|
Set tenant in cache.
|
|
"""
|
|
if not self.cache_enabled:
|
|
return
|
|
|
|
try:
|
|
cache.set(cache_key, tenant, self.cache_timeout)
|
|
except Exception as e:
|
|
logger.error(f"Error setting tenant cache {cache_key}: {e}")
|
|
|
|
def _tenant_cache_key(self, prefix, tenant):
|
|
"""
|
|
Generate cache key for tenant.
|
|
"""
|
|
return f"tenant:{prefix}:{tenant.id}"
|
|
|
|
|
|
class TenantIsolationMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware to enforce tenant isolation at the application level.
|
|
"""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.enforce_isolation = getattr(settings, 'ENFORCE_TENANT_ISOLATION', True)
|
|
self.debug_mode = getattr(settings, 'DEBUG', False)
|
|
|
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
"""
|
|
Validate tenant isolation before view execution.
|
|
"""
|
|
if not self.enforce_isolation:
|
|
return None
|
|
|
|
# Skip isolation checks for public paths
|
|
if not hasattr(request, 'tenant') or not request.tenant:
|
|
return None
|
|
|
|
# Validate that view has access to tenant data
|
|
if hasattr(view_func, 'tenant_required') and view_func.tenant_required:
|
|
if not request.tenant:
|
|
raise PermissionDenied("Tenant context required for this view")
|
|
|
|
# Validate tenant-specific view permissions
|
|
if hasattr(view_func, 'tenant_permissions'):
|
|
required_permissions = view_func.tenant_permissions
|
|
user = get_user(request)
|
|
|
|
if not user.is_authenticated:
|
|
raise PermissionDenied("Authentication required")
|
|
|
|
if not user.is_superuser:
|
|
# Check user permissions against required permissions
|
|
user_permissions = user.get_tenant_permissions()
|
|
for resource, permissions in required_permissions.items():
|
|
if resource not in user_permissions:
|
|
raise PermissionDenied(f"No access to {resource}")
|
|
|
|
user_perms = user_permissions[resource]
|
|
for perm in permissions:
|
|
if perm not in user_perms:
|
|
raise PermissionDenied(f"Missing {perm} permission for {resource}")
|
|
|
|
return None
|
|
|
|
def process_response(self, request, response):
|
|
"""
|
|
Add tenant isolation headers to response.
|
|
"""
|
|
if hasattr(request, 'tenant') and request.tenant:
|
|
response['X-Tenant-ID'] = str(request.tenant.id)
|
|
response['X-Tenant-Slug'] = request.tenant.slug
|
|
|
|
if self.debug_mode:
|
|
response['X-Tenant-Name'] = request.tenant.name
|
|
response['X-Tenant-Plan'] = request.tenant.subscription_plan
|
|
|
|
return response
|
|
|
|
|
|
class TenantActivityMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware to track tenant activity and usage.
|
|
"""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.tracking_enabled = getattr(settings, 'TENANT_ACTIVITY_TRACKING_ENABLED', True)
|
|
|
|
def process_response(self, request, response):
|
|
"""
|
|
Track tenant activity after request completion.
|
|
"""
|
|
if not self.tracking_enabled:
|
|
return response
|
|
|
|
if hasattr(request, 'tenant') and request.tenant:
|
|
# Track API calls
|
|
if request.path.startswith('/api/'):
|
|
self._track_api_usage(request, response)
|
|
|
|
# Track user activity
|
|
user = get_user(request)
|
|
if user.is_authenticated:
|
|
self._track_user_activity(request, user, response)
|
|
|
|
return response
|
|
|
|
def _track_api_usage(self, request, response):
|
|
"""
|
|
Track API usage for tenant.
|
|
"""
|
|
try:
|
|
# This would integrate with your usage tracking system
|
|
# For now, we'll just log the activity
|
|
logger.info(f"API usage tracked for tenant {request.tenant.id}: {request.method} {request.path}")
|
|
except Exception as e:
|
|
logger.error(f"Error tracking API usage: {e}")
|
|
|
|
def _track_user_activity(self, request, user, response):
|
|
"""
|
|
Track user activity within tenant.
|
|
"""
|
|
try:
|
|
# Update user last login time
|
|
if not user.last_login or (timezone.now() - user.last_login).seconds > 300: # 5 minutes
|
|
user.last_login = timezone.now()
|
|
user.save(update_fields=['last_login'])
|
|
|
|
# Log user activity
|
|
logger.info(f"User activity tracked: {user.id} in tenant {request.tenant.id}")
|
|
except Exception as e:
|
|
logger.error(f"Error tracking user activity: {e}")
|
|
|
|
|
|
def tenant_required(view_func):
|
|
"""
|
|
Decorator to mark view as requiring tenant context.
|
|
"""
|
|
view_func.tenant_required = True
|
|
return view_func
|
|
|
|
|
|
def tenant_permissions(**permissions):
|
|
"""
|
|
Decorator to specify required tenant permissions for view.
|
|
|
|
Usage:
|
|
@tenant_permissions(users=['read', 'write'], billing=['read'])
|
|
def my_view(request):
|
|
...
|
|
"""
|
|
def decorator(view_func):
|
|
view_func.tenant_permissions = permissions
|
|
return view_func
|
|
return decorator |