mirror of
https://github.com/suitenumerique/messages.git
synced 2026-09-27 04:04:54 +02:00
Add pagination on maildomain and mailboxes data grid to allow navigation in large list of items
439 lines
17 KiB
Python
439 lines
17 KiB
Python
"""Permission handlers for the messages core app."""
|
|
|
|
from django.core import exceptions
|
|
|
|
from rest_framework import permissions
|
|
|
|
from core import enums, models
|
|
|
|
ACTION_FOR_METHOD_TO_PERMISSION = {
|
|
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"},
|
|
"children": {"GET": "children_list", "POST": "children_create"},
|
|
}
|
|
|
|
|
|
class IsAuthenticated(permissions.BasePermission):
|
|
"""
|
|
Allows access only to authenticated users. Alternative method checking the presence
|
|
of the auth token to avoid hitting the database.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return bool(request.auth) or request.user.is_authenticated
|
|
|
|
|
|
class IsSuperUser(permissions.IsAdminUser):
|
|
"""Allows access only to superusers users."""
|
|
|
|
def has_permission(self, request, view):
|
|
return request.user and (request.user.is_superuser)
|
|
|
|
|
|
class IsAuthenticatedOrSafe(IsAuthenticated):
|
|
"""Allows access to authenticated users (or anonymous users but only on safe methods)."""
|
|
|
|
def has_permission(self, request, view):
|
|
if request.method in permissions.SAFE_METHODS:
|
|
return True
|
|
return super().has_permission(request, view)
|
|
|
|
|
|
class IsSelf(IsAuthenticated):
|
|
"""
|
|
Allows access only to authenticated users. Alternative method checking the presence
|
|
of the auth token to avoid hitting the database.
|
|
"""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Write permissions are only allowed to the user itself."""
|
|
return obj == request.user
|
|
|
|
|
|
class IsOwnedOrPublic(IsAuthenticated):
|
|
"""
|
|
Allows access to authenticated users only for objects that are owned or not related
|
|
to any user via the "owner" field.
|
|
"""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Unsafe permissions are only allowed for the owner of the object."""
|
|
if obj.owner == request.user:
|
|
return True
|
|
|
|
if request.method in permissions.SAFE_METHODS and obj.owner is None:
|
|
return True
|
|
|
|
try:
|
|
return obj.user == request.user
|
|
except exceptions.ObjectDoesNotExist:
|
|
return False
|
|
|
|
|
|
class AccessPermission(permissions.BasePermission):
|
|
"""Permission class for access objects."""
|
|
|
|
def has_permission(self, request, view):
|
|
return request.user.is_authenticated or view.action != "create"
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check permission for a given object."""
|
|
abilities = obj.get_abilities(request.user)
|
|
action = view.action
|
|
try:
|
|
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
|
|
except KeyError:
|
|
pass
|
|
return abilities.get(action, False)
|
|
|
|
|
|
class IsAllowedToAccess(IsAuthenticated):
|
|
"""Permission class for access to a mailbox context or specific threads/messages."""
|
|
|
|
def has_permission(self, request, view):
|
|
"""Check if user has permission to access the mailbox thread list or message list.
|
|
Only role VIEWER is required to access the mailbox threads/messages.
|
|
So we just need to check if user has any access role on mailbox or thread.
|
|
"""
|
|
|
|
if not IsAuthenticated.has_permission(self, request, view):
|
|
return False
|
|
|
|
# This check is primarily for LIST actions based on query params
|
|
mailbox_id = request.query_params.get("mailbox_id") # Used by Thread list
|
|
thread_id = request.query_params.get("thread_id") # Used by Message list
|
|
|
|
# If it's a detail action (retrieve, update, destroy), object-level permission is checked
|
|
# by has_object_permission. If it's a list action without filters, deny access.
|
|
is_list_action = hasattr(view, "action") and view.action == "list"
|
|
|
|
if not is_list_action:
|
|
# Allow non-list actions (like detail views or specific APIViews like SendMessageView)
|
|
# to proceed to object-level checks or handle permissions within the view.
|
|
return True
|
|
|
|
# --- The following logic only applies if is_list_action is True --- #
|
|
# Check access based on query params for LIST action
|
|
if mailbox_id:
|
|
# Check if the user has access to this specific mailbox to list threads
|
|
return models.Mailbox.objects.filter(
|
|
id=mailbox_id, accesses__user=request.user
|
|
).exists()
|
|
if thread_id:
|
|
# Check if the user has access to this specific thread to list messages
|
|
return models.ThreadAccess.objects.filter(
|
|
thread_id=thread_id, mailbox__accesses__user=request.user
|
|
).exists()
|
|
|
|
return False # Should not be reached if logic above is correct
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check if user has permission to access the specific object (Message, Thread, Mailbox)."""
|
|
user = request.user
|
|
if isinstance(obj, models.Mailbox):
|
|
# Check access directly on the mailbox
|
|
return models.MailboxAccess.objects.filter(mailbox=obj, user=user).exists()
|
|
|
|
if isinstance(obj, (models.Message, models.Thread)):
|
|
thread = obj.thread if isinstance(obj, models.Message) else obj
|
|
# Check access via the message's thread using ThreadAccess
|
|
# First, just check if *any* access exists for the user to this thread.
|
|
has_access = models.ThreadAccess.objects.filter(
|
|
thread=thread, mailbox__accesses__user=user
|
|
).exists()
|
|
if not has_access:
|
|
return False
|
|
|
|
# Only EDITOR, SENDER or ADMIN role can destroy. SENDER or ADMIN can send.
|
|
if view.action in ["destroy", "send"]:
|
|
mailbox = thread.accesses.get(mailbox__accesses__user=user).mailbox
|
|
if (
|
|
models.ThreadAccess.objects.filter(
|
|
thread=thread,
|
|
mailbox=mailbox,
|
|
role=enums.ThreadAccessRoleChoices.EDITOR,
|
|
).exists()
|
|
and models.MailboxAccess.objects.filter(
|
|
mailbox=mailbox,
|
|
user=user,
|
|
role__in=[
|
|
enums.MailboxRoleChoices.ADMIN,
|
|
enums.MailboxRoleChoices.SENDER,
|
|
]
|
|
+ (
|
|
[enums.MailboxRoleChoices.EDITOR]
|
|
if view.action == "destroy"
|
|
else []
|
|
),
|
|
).exists()
|
|
):
|
|
return True
|
|
# for retrieve action has_access is already checked above
|
|
else:
|
|
return True
|
|
|
|
# Deny access for other object types or if type is unknown
|
|
return False
|
|
|
|
|
|
class IsAllowedToCreateMessage(IsAuthenticated):
|
|
"""Permission class for access to create a message."""
|
|
|
|
def has_permission(self, request, view):
|
|
"""Check if user is allowed to create a message."""
|
|
|
|
if not IsAuthenticated.has_permission(self, request, view):
|
|
return False
|
|
|
|
# a sender mailbox is required to create/send a message
|
|
sender_id = request.data.get("senderId")
|
|
parent_id = request.data.get("parentId")
|
|
if not sender_id:
|
|
return False
|
|
|
|
# get mailbox instance from sender id
|
|
try:
|
|
# Store mailbox on the view for later use (e.g., in the view logic)
|
|
view.mailbox = models.Mailbox.objects.get(id=sender_id)
|
|
except models.Mailbox.DoesNotExist:
|
|
return False # Invalid senderId
|
|
|
|
# Check if user has required role on the sender Mailbox
|
|
has_edit_role = view.mailbox.accesses.filter(
|
|
user=request.user,
|
|
role__in=[
|
|
enums.MailboxRoleChoices.EDITOR,
|
|
enums.MailboxRoleChoices.ADMIN,
|
|
enums.MailboxRoleChoices.SENDER,
|
|
],
|
|
).exists()
|
|
|
|
# if user does not have edit role with this sender mailbox, return False
|
|
if not has_edit_role:
|
|
return False
|
|
|
|
# --- Additional check for replies ---
|
|
# If creating a reply (parentId is provided), check access to the parent thread
|
|
if parent_id:
|
|
try:
|
|
parent_message = models.Message.objects.select_related("thread").get(
|
|
id=parent_id
|
|
)
|
|
# Check if the user has access to the thread they are replying to
|
|
if models.ThreadAccess.objects.filter(
|
|
thread=parent_message.thread,
|
|
mailbox=view.mailbox,
|
|
role=models.ThreadAccessRoleChoices.EDITOR,
|
|
).exists():
|
|
return True
|
|
except models.Message.DoesNotExist:
|
|
return False # Treat invalid parentId as permission failure
|
|
|
|
# --- Additional check for updating existing draft ---
|
|
# If updating (messageId is provided), check access to the draft's thread
|
|
message_id = request.data.get("messageId")
|
|
if message_id and request.method == "PUT": # Check only needed for updates
|
|
try:
|
|
draft_message = models.Message.objects.select_related("thread").get(
|
|
id=message_id, is_draft=True
|
|
)
|
|
# Check if the user has access to the thread of the draft being updated
|
|
if not models.ThreadAccess.objects.filter(
|
|
thread=draft_message.thread,
|
|
mailbox=view.mailbox,
|
|
role=models.ThreadAccessRoleChoices.EDITOR,
|
|
).exists():
|
|
return False
|
|
except models.Message.DoesNotExist:
|
|
# Let the view handle invalid messageId
|
|
return False # Treat invalid messageId as permission failure
|
|
|
|
# If all checks pass
|
|
return True
|
|
|
|
|
|
class IsAllowedToManageThreadAccess(IsAuthenticated):
|
|
"""Permission class for access to create, update, delete and list thread accesses."""
|
|
|
|
def has_permission(self, request, view):
|
|
# Get thread_id from URL kwargs instead of query params/data
|
|
thread_id = view.kwargs.get("thread_id")
|
|
if not thread_id:
|
|
return False
|
|
|
|
# if create action, check if user has admin/editor access to the mailbox and the thread access role is editor
|
|
if view.action == "create":
|
|
# authenticated user wants to create a thread access for a specific thread
|
|
# check if user has admin/editor access to the mailbox and the
|
|
# thread access role is editor already exists for them
|
|
return (
|
|
models.ThreadAccess.objects.select_related("mailbox")
|
|
.filter(
|
|
thread_id=thread_id,
|
|
mailbox__accesses__user=request.user,
|
|
mailbox__accesses__role__in=[
|
|
enums.MailboxRoleChoices.ADMIN,
|
|
enums.MailboxRoleChoices.EDITOR,
|
|
enums.MailboxRoleChoices.SENDER,
|
|
],
|
|
role=enums.ThreadAccessRoleChoices.EDITOR,
|
|
)
|
|
.exists()
|
|
)
|
|
if view.action == "list":
|
|
# list is only allowed for a user with access to the thread
|
|
return (
|
|
models.ThreadAccess.objects.select_related("mailbox")
|
|
.filter(
|
|
thread_id=thread_id,
|
|
mailbox__accesses__user=request.user,
|
|
mailbox__accesses__role__in=[
|
|
enums.MailboxRoleChoices.ADMIN,
|
|
enums.MailboxRoleChoices.EDITOR,
|
|
enums.MailboxRoleChoices.SENDER,
|
|
],
|
|
role=enums.ThreadAccessRoleChoices.EDITOR,
|
|
)
|
|
.exists()
|
|
)
|
|
|
|
return True # to proceed to object-level checks
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check if user has permission to access the specific object (ThreadAccess).
|
|
Manage retrieve, update, destroy actions here.
|
|
"""
|
|
# Verify the thread access belongs to the thread in the URL
|
|
if obj.thread.id != view.kwargs.get("thread_id"):
|
|
return False
|
|
|
|
return (
|
|
models.ThreadAccess.objects.select_related("mailbox")
|
|
.filter(
|
|
thread=obj.thread,
|
|
mailbox__accesses__user=request.user,
|
|
mailbox__accesses__role__in=[
|
|
enums.MailboxRoleChoices.ADMIN,
|
|
enums.MailboxRoleChoices.EDITOR,
|
|
enums.MailboxRoleChoices.SENDER,
|
|
],
|
|
role=enums.ThreadAccessRoleChoices.EDITOR,
|
|
)
|
|
.exists()
|
|
)
|
|
|
|
|
|
class IsMailDomainAdmin(permissions.BasePermission):
|
|
"""
|
|
Allows access only to users who have ADMIN MailDomainAccess
|
|
to the maildomain specified by 'maildomain_pk' in the URL.
|
|
Used for viewsets nested under a maildomain.
|
|
If no maildomain_pk is provided, the permission checks if the user
|
|
is at least an ADMIN of one maildomain.
|
|
"""
|
|
|
|
message = "You do not have administrative rights for this mail domain."
|
|
|
|
def has_permission(self, request, view):
|
|
if not request.user or not request.user.is_authenticated:
|
|
return False
|
|
|
|
qs_filters = {
|
|
"user": request.user,
|
|
"role": models.MailDomainAccessRoleChoices.ADMIN,
|
|
}
|
|
maildomain_pk = view.kwargs.get("maildomain_pk")
|
|
if maildomain_pk:
|
|
qs_filters["maildomain_id"] = maildomain_pk
|
|
|
|
return models.MailDomainAccess.objects.filter(**qs_filters).exists()
|
|
|
|
# No has_object_permission, assumes objects are correctly scoped by view's get_queryset
|
|
# based on the maildomain_pk.
|
|
|
|
|
|
class IsMailboxAdmin(permissions.BasePermission):
|
|
"""
|
|
Allows access if the user has ADMIN MailboxAccess to the specific Mailbox
|
|
identified by `view.kwargs['mailbox_id']`, OR if the user has ADMIN
|
|
MailDomainAccess to the domain of that Mailbox.
|
|
"""
|
|
|
|
message = "You do not have administrative rights for this mailbox or its domain."
|
|
|
|
def has_permission(self, request, view):
|
|
if not request.user or not request.user.is_authenticated:
|
|
return False
|
|
|
|
user = request.user
|
|
mailbox_id_from_url = view.kwargs.get("mailbox_id")
|
|
if not mailbox_id_from_url:
|
|
return False # Should not happen with correct URL configuration
|
|
|
|
try:
|
|
target_mailbox = models.Mailbox.objects.select_related("domain").get(
|
|
pk=mailbox_id_from_url
|
|
)
|
|
except (models.Mailbox.DoesNotExist, ValueError): # ValueError for invalid UUID
|
|
return False
|
|
|
|
# Check 1 : Is user a super user ?
|
|
if user.is_superuser:
|
|
return True
|
|
|
|
# Check 2: Is user an admin of the specific mailbox?
|
|
is_mailbox_admin = models.MailboxAccess.objects.filter(
|
|
user=user, mailbox=target_mailbox, role=models.MailboxRoleChoices.ADMIN
|
|
).exists()
|
|
|
|
if is_mailbox_admin:
|
|
return True
|
|
|
|
# Check 3: Is user an admin of the mailbox's domain?
|
|
if target_mailbox.domain:
|
|
is_domain_admin = models.MailDomainAccess.objects.filter(
|
|
user=user,
|
|
maildomain=target_mailbox.domain,
|
|
role=models.MailDomainAccessRoleChoices.ADMIN,
|
|
).exists()
|
|
if is_domain_admin:
|
|
return True
|
|
|
|
return False
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
# obj is a MailboxAccess instance.
|
|
if not request.user or not request.user.is_authenticated:
|
|
return False
|
|
|
|
if not hasattr(obj, "mailbox") or not obj.mailbox or not obj.mailbox.domain:
|
|
return False # MailboxAccess must be linked to a Mailbox with a Domain
|
|
|
|
# Ensure the object being acted upon belongs to the mailbox specified in the URL
|
|
mailbox_id_from_url = view.kwargs.get("mailbox_id")
|
|
if str(obj.mailbox.id) != str(mailbox_id_from_url):
|
|
return False # Object's mailbox does not match URL mailbox
|
|
|
|
user = request.user
|
|
target_mailbox = obj.mailbox # The mailbox related to the MailboxAccess object
|
|
|
|
# Check 1 : Is user a super user ?
|
|
if user.is_superuser:
|
|
return True
|
|
|
|
# Check 2: Is user an admin of this specific mailbox?
|
|
is_mailbox_admin = models.MailboxAccess.objects.filter(
|
|
user=user, mailbox=target_mailbox, role=models.MailboxRoleChoices.ADMIN
|
|
).exists()
|
|
|
|
if is_mailbox_admin:
|
|
return True
|
|
|
|
# Check 3: Is user an admin of the mailbox's domain?
|
|
is_domain_admin = models.MailDomainAccess.objects.filter(
|
|
user=user,
|
|
maildomain=target_mailbox.domain,
|
|
role=models.MailDomainAccessRoleChoices.ADMIN,
|
|
).exists()
|
|
|
|
return is_domain_admin
|