mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 10:31:09 -05:00
- 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
465 lines
16 KiB
Python
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
|