Files
thrillwiki_django_no_react/backend/apps/moderation/permissions.py

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