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
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:
603
backend/src/core/middleware/tenant_middleware.py
Normal file
603
backend/src/core/middleware/tenant_middleware.py
Normal 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
|
||||
Reference in New Issue
Block a user