""" Smooth Schedule Hijack Permissions Implements the masquerading "Matrix" - strict rules for who can impersonate whom """ from django.core.exceptions import PermissionDenied def can_hijack(hijacker, hijacked): """ Determine if hijacker (the admin) can masquerade as hijacked (target user). The Matrix: ┌──────────────────────┬─────────────────────────────────────────────────┐ │ Hijacker Role │ Can Hijack │ ├──────────────────────┼─────────────────────────────────────────────────┤ │ SUPERUSER │ Anyone (full god mode) │ │ PLATFORM_SUPPORT │ TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF │ │ PLATFORM_SALES │ Only users with is_temporary=True │ │ TENANT_OWNER │ TENANT_STAFF in same tenant only │ │ Others │ Nobody │ └──────────────────────┴─────────────────────────────────────────────────┘ Args: hijacker: User attempting to impersonate (the admin) hijacked: User being impersonated (the target) Returns: bool: True if hijack is allowed, False otherwise Security Notes: - Never allow self-hijacking - Never allow hijacking of superusers (except by other superusers) - Always validate tenant boundaries for tenant-scoped roles - Log all hijack attempts (success and failure) for audit """ from users.models import User # Safety check: can't hijack yourself if hijacker.id == hijacked.id: return False # Safety check: only superusers can hijack other superusers if hijacked.role == User.Role.SUPERUSER and hijacker.role != User.Role.SUPERUSER: return False # Rule 1: SUPERUSER can hijack anyone if hijacker.role == User.Role.SUPERUSER: return True # Rule 2: PLATFORM_SUPPORT can hijack tenant users if hijacker.role == User.Role.PLATFORM_SUPPORT: return hijacked.role in [ User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER, User.Role.TENANT_STAFF, User.Role.CUSTOMER, ] # Rule 3: PLATFORM_SALES can only hijack temporary demo accounts if hijacker.role == User.Role.PLATFORM_SALES: return hijacked.is_temporary # Rule 4: TENANT_OWNER can hijack staff within their own tenant if hijacker.role == User.Role.TENANT_OWNER: # Must be in same tenant if not hijacker.tenant or not hijacked.tenant: return False if hijacker.tenant.id != hijacked.tenant.id: return False # Can only hijack staff and customers, not other owners/managers return hijacked.role in [ User.Role.TENANT_STAFF, User.Role.CUSTOMER, ] # Default: deny return False def can_hijack_or_403(hijacker, hijacked): """ Same as can_hijack but raises PermissionDenied instead of returning False. Useful for views that want to use exception handling. Args: hijacker: User attempting to impersonate hijacked: User being impersonated Raises: PermissionDenied: If hijack is not allowed Returns: bool: True if allowed (never returns False, raises instead) """ if not can_hijack(hijacker, hijacked): raise PermissionDenied( f"User {hijacker.email} ({hijacker.get_role_display()}) " f"is not authorized to masquerade as {hijacked.email} ({hijacked.get_role_display()})" ) return True def get_hijackable_users(hijacker): """ Get queryset of all users that the hijacker can masquerade as. Useful for building "Masquerade as..." dropdowns in the UI. Args: hijacker: User who wants to hijack Returns: QuerySet: Users that can be hijacked by this user """ from users.models import User # Start with all users except self qs = User.objects.exclude(id=hijacker.id) # Apply filters based on hijacker role if hijacker.role == User.Role.SUPERUSER: # Superuser can hijack anyone return qs elif hijacker.role == User.Role.PLATFORM_SUPPORT: # Can hijack all tenant-level users return qs.filter(role__in=[ User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER, User.Role.TENANT_STAFF, User.Role.CUSTOMER, ]) elif hijacker.role == User.Role.PLATFORM_SALES: # Only temporary demo accounts return qs.filter(is_temporary=True) elif hijacker.role == User.Role.TENANT_OWNER: # Only staff in same tenant if not hijacker.tenant: return qs.none() return qs.filter( tenant=hijacker.tenant, role__in=[User.Role.TENANT_STAFF, User.Role.CUSTOMER] ) else: # No one else can hijack return qs.none() def validate_hijack_chain(request): """ Validate that hijack chains are not too deep. Prevents: Admin1 -> Admin2 -> Admin3 -> User scenarios. Smooth Schedule Security Policy: Maximum hijack depth is 1. You cannot hijack while already hijacked. Args: request: Django request object Raises: PermissionDenied: If already in a hijack session Returns: bool: True if allowed to start new hijack """ hijack_history = request.session.get('hijack_history', []) if len(hijack_history) > 0: raise PermissionDenied( "Cannot start a new masquerade session while already masquerading. " "Please exit your current session first." ) return True # ============================================================================== # SaaS Quota Enforcement ("Hard Block" Logic) # ============================================================================== def HasQuota(feature_code): """ Permission factory for SaaS tier limit enforcement. Returns a DRF permission class that blocks write operations when tenant has exceeded their quota for a specific feature. The "Hard Block": Prevents resource creation when tenant hits limit. Usage: class ResourceViewSet(ModelViewSet): permission_classes = [IsAuthenticated, HasQuota('MAX_RESOURCES')] Args: feature_code: TierLimit feature code (e.g., 'MAX_RESOURCES', 'MAX_USERS') Returns: QuotaPermission class configured for the feature How it Works: 1. Read operations (GET/HEAD/OPTIONS) always allowed 2. Write operations check current usage vs tier limit 3. If usage >= limit, raises PermissionDenied (403) """ from rest_framework.permissions import BasePermission from django.apps import apps class QuotaPermission(BasePermission): """ Dynamically generated permission class for quota checking. """ # Map feature codes to model paths for usage counting # CRITICAL: This map must be populated for the permission to work USAGE_MAP = { 'MAX_RESOURCES': 'schedule.Resource', 'MAX_USERS': 'users.User', 'MAX_EVENTS_PER_MONTH': 'schedule.Event', # Add more mappings as needed } def has_permission(self, request, view): """ Check if tenant has quota for this operation. Returns True for read operations, checks quota for writes. """ # Allow all read-only operations (GET, HEAD, OPTIONS) if request.method in ['GET', 'HEAD', 'OPTIONS']: return True # Get tenant from request tenant = getattr(request, 'tenant', None) if not tenant: # No tenant in request - allow for public schema operations return True # Get the model to check usage against model_path = self.USAGE_MAP.get(feature_code) if not model_path: # Feature not mapped - fail safe by allowing # (Production: you'd want to log this as a configuration error) return True # Get the model class try: app_label, model_name = model_path.split('.') Model = apps.get_model(app_label, model_name) except (ValueError, LookupError): # Invalid model path - fail safe return True # Get the tier limit for this tenant try: from core.models import TierLimit limit = TierLimit.objects.get( tier=tenant.subscription_tier, feature_code=feature_code ).limit except TierLimit.DoesNotExist: # No limit defined - allow (unlimited) return True # Count current usage # NOTE: django-tenants automatically scopes this query to tenant schema current_count = Model.objects.count() # The "Hard Block": Enforce the limit if current_count >= limit: # Quota exceeded - deny the operation from rest_framework.exceptions import PermissionDenied raise PermissionDenied( f"Quota exceeded: You have reached your plan limit of {limit} " f"{feature_code.replace('MAX_', '').lower().replace('_', ' ')}. " f"Please upgrade your subscription to add more." ) # Quota available - allow the operation return True return QuotaPermission