Now that we have introduced a document tree structure, it is not possible to allow deleting documents anymore as it impacts the whole subtree below the deleted document and the consequences are too big. We introduce soft delete in order to give a second thought to the document's owner (who is the only one to be allowed to delete a document). After a document is soft deleted, the owner can still see it in the trashbin (/api/v1.0/documents/trashbin). After a grace period (30 days be default) the document disappears from the trashbin and can't be restored anymore. Note that even then it is still kept in database. Cleaning the database to erase deleted documents after the grace period can be done as a maintenance script.
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
"""Permission handlers for the impress core app."""
|
|
|
|
from django.core import exceptions
|
|
from django.db.models import Q
|
|
from django.http import Http404
|
|
|
|
from rest_framework import permissions
|
|
|
|
from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff
|
|
|
|
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 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 CanCreateInvitationPermission(permissions.BasePermission):
|
|
"""
|
|
Custom permission class to handle permission checks for managing invitations.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
user = request.user
|
|
|
|
# Ensure the user is authenticated
|
|
if not (bool(request.auth) or request.user.is_authenticated):
|
|
return False
|
|
|
|
# Apply permission checks only for creation (POST requests)
|
|
if view.action != "create":
|
|
return True
|
|
|
|
# Check if resource_id is passed in the context
|
|
try:
|
|
document_id = view.kwargs["resource_id"]
|
|
except KeyError as exc:
|
|
raise exceptions.ValidationError(
|
|
"You must set a document ID in kwargs to manage document invitations."
|
|
) from exc
|
|
|
|
# Check if the user has access to manage invitations (Owner/Admin roles)
|
|
return DocumentAccess.objects.filter(
|
|
Q(user=user) | Q(team__in=user.teams),
|
|
document=document_id,
|
|
role__in=[RoleChoices.OWNER, RoleChoices.ADMIN],
|
|
).exists()
|
|
|
|
|
|
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 DocumentAccessPermission(AccessPermission):
|
|
"""Subclass to handle soft deletion specificities."""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""
|
|
Return a 404 on deleted documents
|
|
- for which the trashbin cutoff is past
|
|
- for which the current user is not owner of the document or one of its ancestors
|
|
"""
|
|
if (
|
|
deleted_at := obj.ancestors_deleted_at
|
|
) and deleted_at < get_trashbin_cutoff():
|
|
raise Http404
|
|
|
|
# Compute permission first to ensure the "user_roles" attribute is set
|
|
has_permission = super().has_object_permission(request, view, obj)
|
|
|
|
if obj.ancestors_deleted_at and not RoleChoices.OWNER in obj.user_roles:
|
|
raise Http404
|
|
|
|
return has_permission
|