project initialization
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

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,603 @@
"""
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