""" Moderation Permissions This module contains custom permission classes for the moderation system, providing role-based access control for moderation operations. Each permission class includes an `as_guard()` class method that converts the permission to an FSM guard function, enabling alignment between API permissions and FSM transition checks. """ from collections.abc import Callable from typing import Any from django.contrib.auth import get_user_model from rest_framework import permissions User = get_user_model() class PermissionGuardAdapter: """ Adapter that wraps a DRF permission class as an FSM guard. This allows DRF permission classes to be used as conditions for FSM transitions, ensuring consistent authorization between API endpoints and state transitions. Example: guard = IsModeratorOrAdmin.as_guard() # Use in FSM transition conditions @transition(conditions=[guard]) def approve(self, user=None): pass """ def __init__( self, permission_class: type, error_message: str | None = None, ): """ Initialize the guard adapter. Args: permission_class: The DRF permission class to adapt error_message: Custom error message on failure """ self.permission_class = permission_class self._custom_error_message = error_message self._last_error_code: str | None = None @property def error_code(self) -> str | None: """Return the error code from the last failed check.""" return self._last_error_code def __call__(self, instance: Any, user: Any = None) -> bool: """ Check if the permission passes for the given user. Args: instance: Model instance being transitioned user: User attempting the transition Returns: True if the permission check passes """ self._last_error_code = None if user is None: self._last_error_code = "NO_USER" return False # Create a mock request object for DRF permission check class MockRequest: def __init__(self, user): self.user = user self.data = {} self.method = "POST" mock_request = MockRequest(user) permission = self.permission_class() # Check permission if not permission.has_permission(mock_request, None): self._last_error_code = "PERMISSION_DENIED" return False # Check object permission if available if hasattr(permission, "has_object_permission"): if not permission.has_object_permission(mock_request, None, instance): self._last_error_code = "OBJECT_PERMISSION_DENIED" return False return True def get_error_message(self) -> str: """Return user-friendly error message.""" if self._custom_error_message: return self._custom_error_message return f"Permission denied by {self.permission_class.__name__}" def get_required_roles(self) -> list: """Return list of roles that would satisfy this permission.""" # Try to infer from permission class name name = self.permission_class.__name__ if "Superuser" in name: return ["SUPERUSER"] elif "Admin" in name: return ["ADMIN", "SUPERUSER"] elif "Moderator" in name: return ["MODERATOR", "ADMIN", "SUPERUSER"] return ["USER", "MODERATOR", "ADMIN", "SUPERUSER"] class GuardMixin: """ Mixin that adds guard adapter functionality to DRF permission classes. """ @classmethod def as_guard(cls, error_message: str | None = None) -> Callable: """ Convert this permission class to an FSM guard function. Args: error_message: Optional custom error message Returns: Guard function compatible with FSM transition conditions Example: guard = IsModeratorOrAdmin.as_guard() # In transition definition @transition(conditions=[guard]) def approve(self, user=None): pass """ return PermissionGuardAdapter(cls, error_message=error_message) class IsModerator(GuardMixin, permissions.BasePermission): """ Permission that only allows moderators to access the view. Use `IsModerator.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has moderator role.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role == "MODERATOR" def has_object_permission(self, request, view, obj): """Check object-level permissions for moderators.""" return self.has_permission(request, view) class IsModeratorOrAdmin(GuardMixin, permissions.BasePermission): """ Permission that allows moderators, admins, and superusers to access the view. Use `IsModeratorOrAdmin.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has moderator, admin, or superuser role.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["MODERATOR", "ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for moderators and admins.""" return self.has_permission(request, view) class IsAdminOrSuperuser(GuardMixin, permissions.BasePermission): """ Permission that only allows admins and superusers to access the view. Use `IsAdminOrSuperuser.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has admin or superuser role.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for admins and superusers.""" return self.has_permission(request, view) class CanViewModerationData(GuardMixin, permissions.BasePermission): """ Permission that allows users to view moderation data based on their role. - Regular users can only view their own reports - Moderators and above can view all moderation data Use `CanViewModerationData.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated.""" return request.user and request.user.is_authenticated def has_object_permission(self, request, view, obj): """Check object-level permissions for viewing moderation data.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") # Moderators and above can view all data if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]: return True # Regular users can only view their own reports if hasattr(obj, "reported_by"): return obj.reported_by == request.user # For other objects, deny access to regular users return False class CanModerateContent(GuardMixin, permissions.BasePermission): """ Permission that allows users to moderate content based on their role. - Only moderators and above can moderate content - Includes additional checks for specific moderation actions Use `CanModerateContent.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has moderation privileges.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["MODERATOR", "ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for content moderation.""" if not self.has_permission(request, view): return False user_role = getattr(request.user, "role", "USER") # Superusers can do everything if user_role == "SUPERUSER": return True # Admins can moderate most content but may have some restrictions if user_role == "ADMIN": # Add any admin-specific restrictions here if needed return True # Moderators have basic moderation permissions if user_role == "MODERATOR": # Add any moderator-specific restrictions here if needed # For example, moderators might not be able to moderate admin actions if hasattr(obj, "moderator") and obj.moderator: moderator_role = getattr(obj.moderator, "role", "USER") if moderator_role in ["ADMIN", "SUPERUSER"]: return False return True return False class CanAssignModerationTasks(GuardMixin, permissions.BasePermission): """ Permission that allows users to assign moderation tasks to others. - Moderators can assign tasks to themselves - Admins can assign tasks to moderators and themselves - Superusers can assign tasks to anyone Use `CanAssignModerationTasks.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has assignment privileges.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["MODERATOR", "ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for task assignment.""" if not self.has_permission(request, view): return False user_role = getattr(request.user, "role", "USER") # Superusers can assign to anyone if user_role == "SUPERUSER": return True # Admins can assign to moderators and themselves if user_role == "ADMIN": return True # Moderators can only assign to themselves if user_role == "MODERATOR": # Check if they're trying to assign to themselves assignee_id = request.data.get("moderator_id") or request.data.get( "assigned_to" ) if assignee_id: return str(assignee_id) == str(request.user.id) return True return False class CanPerformBulkOperations(GuardMixin, permissions.BasePermission): """ Permission that allows users to perform bulk operations. - Only admins and superusers can perform bulk operations - Includes additional safety checks for destructive operations Use `CanPerformBulkOperations.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has bulk operation privileges.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for bulk operations.""" if not self.has_permission(request, view): return False user_role = getattr(request.user, "role", "USER") # Superusers can perform all bulk operations if user_role == "SUPERUSER": return True # Admins can perform most bulk operations if user_role == "ADMIN": # Add any admin-specific restrictions for bulk operations here # For example, admins might not be able to perform certain destructive operations operation_type = getattr(obj, "operation_type", None) if operation_type in ["DELETE_USERS", "PURGE_DATA"]: return False # Only superusers can perform these operations return True return False class IsOwnerOrModerator(GuardMixin, permissions.BasePermission): """ Permission that allows object owners or moderators to access the view. - Users can access their own objects - Moderators and above can access any object Use `IsOwnerOrModerator.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated.""" return request.user and request.user.is_authenticated def has_object_permission(self, request, view, obj): """Check object-level permissions for owners or moderators.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") # Moderators and above can access any object if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]: return True # Check if user is the owner of the object if hasattr(obj, "reported_by"): return obj.reported_by == request.user elif hasattr(obj, "created_by"): return obj.created_by == request.user elif hasattr(obj, "user"): return obj.user == request.user return False class CanManageUserRestrictions(GuardMixin, permissions.BasePermission): """ Permission that allows users to manage user restrictions and moderation actions. - Moderators can create basic restrictions (warnings, temporary suspensions) - Admins can create more severe restrictions (longer suspensions, content removal) - Superusers can create any restriction including permanent bans Use `CanManageUserRestrictions.as_guard()` to get an FSM-compatible guard. """ def has_permission(self, request, view): """Check if user is authenticated and has restriction management privileges.""" if not request.user or not request.user.is_authenticated: return False user_role = getattr(request.user, "role", "USER") return user_role in ["MODERATOR", "ADMIN", "SUPERUSER"] def has_object_permission(self, request, view, obj): """Check object-level permissions for managing user restrictions.""" if not self.has_permission(request, view): return False user_role = getattr(request.user, "role", "USER") # Superusers can manage any restriction if user_role == "SUPERUSER": return True # Get the action type from request data or object action_type = None if request.method in ["POST", "PUT", "PATCH"]: action_type = request.data.get("action_type") elif hasattr(obj, "action_type"): action_type = obj.action_type # Admins can manage most restrictions if user_role == "ADMIN": # Admins cannot create permanent bans return not (action_type == "USER_BAN" and request.data.get("duration_hours") is None) # Moderators can only manage basic restrictions if user_role == "MODERATOR": allowed_actions = ["WARNING", "CONTENT_REMOVAL", "USER_SUSPENSION"] if action_type not in allowed_actions: return False # Moderators can only create temporary suspensions (max 7 days) if action_type == "USER_SUSPENSION": duration_hours = request.data.get("duration_hours", 0) if duration_hours > 168: # 7 days = 168 hours return False return True return False