Files
thrillwiki_django_no_react/backend/apps/moderation/permissions.py
pacnpal 7ba0004c93 chore: fix pghistory migration deps and improve htmx utilities
- Update pghistory dependency from 0007 to 0006 in account migrations
- Add docstrings and remove unused imports in htmx_forms.py
- Add DJANGO_SETTINGS_MODULE bash commands to Claude settings
- Add state transition definitions for ride statuses
2025-12-21 17:33:24 -05:00

465 lines
16 KiB
Python

"""
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 typing import Callable, Any, Optional
from rest_framework import permissions
from django.contrib.auth import get_user_model
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: Optional[str] = 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: Optional[str] = None
@property
def error_code(self) -> Optional[str]:
"""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: Optional[str] = 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
if action_type == "USER_BAN" and request.data.get("duration_hours") is None:
return False
return True
# 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