Corp/backend/api/v1/documents/permissions.py
lizerk 5fd9d9bf94 WIP: separation of duties implementation (incomplete)
Added:
- is_not_document_uploader predicate
- Specific FSM action permissions (approve_document, publish_document, etc.)
- Tests for author cannot approve/publish own documents

Note: This is work in progress and will be reviewed/simplified after comparing with develop branch
2026-03-05 15:04:22 +03:00

121 lines
5.7 KiB
Python

# backend/api/v1/documents/permissions.py
from __future__ import annotations
from typing import Any
from copy import deepcopy
from django.contrib.auth.models import AbstractBaseUser
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.request import Request
from rest_framework.views import View
from ..permissions import AdminOrActionOrModelPerms
def _has_any_model_perm(
user, app_label: str, model_name: str, actions=("view", "change", "add", "delete")
) -> bool:
"""Проверяет наличие любого модельного права из набора."""
for act in actions:
code = f"{act}_{model_name}"
if user.has_perm(f"{app_label}.{code}"):
return True
return False
class DocumentReadOrModelPerms(AdminOrActionOrModelPerms):
"""Чтение/скачивание/ознакомление: любой аутентифицированный,
но доступ только к 'своим' документам (или если есть модельные права/админ).
Создание/редактирование/удаление: только админы и обладатели модельных прав.
Raises:
PermissionDenied: если нет доступа к конкретному документу.
"""
perms_map = deepcopy(DjangoModelPermissions.perms_map)
perms_map["GET"] = []
perms_map["HEAD"] = []
perms_map["OPTIONS"] = []
# FSM workflow actions - разрешаем на основе object-level permissions
FSM_ACTIONS = (
"submit_for_review",
"approve",
"reject",
"publish",
"archive",
"unarchive",
"return_to_draft",
"revert",
)
def has_permission(self, request: Request, view: View) -> bool:
action = getattr(view, "action", None)
# SAFE + acknowledge/download → достаточно быть аутентифицированным
if request.method in SAFE_METHODS or action in ("acknowledge", "download"):
return bool(request.user and request.user.is_authenticated)
# CREATE → разрешаем всем аутентифицированным (документы создаются в draft)
# FSM workflow обеспечит контроль: draft → review → approve → publish
if action == "create":
return bool(request.user and request.user.is_authenticated)
# FSM actions, update/destroy и related documents - проверяем на уровне объекта
# Разрешаем здесь, has_object_permission проверит django-rules
if action in self.FSM_ACTIONS or action in ("update", "partial_update", "destroy", "add_related", "remove_related"):
return bool(request.user and request.user.is_authenticated)
# небезопасные → staff ИЛИ стандартные модельные права (add/change/delete)
if request.user and request.user.is_staff:
return True
return super().has_permission(request, view)
def has_object_permission(self, request: Request, view: View, obj: Any) -> bool:
user = request.user
if not (user and user.is_authenticated):
return False
action = getattr(view, "action", None)
# Полный доступ при наличии ЛЮБОГО модельного права или staff
if user.is_staff or _has_any_model_perm(user, "documents", "document"):
return True
# FSM actions → проверяем специфичные django-rules права (разделение обязанностей!)
if action == "submit_for_review":
return user.has_perm("documents.submit_for_review_document", obj)
elif action == "approve":
return user.has_perm("documents.approve_document", obj)
elif action == "publish":
return user.has_perm("documents.publish_document", obj)
elif action == "reject":
return user.has_perm("documents.reject_document", obj)
elif action in ("archive", "unarchive"):
return user.has_perm("documents.archive_document", obj)
elif action == "return_to_draft":
# Вернуть в draft может автор или менеджер
return user.has_perm("documents.change_document", obj)
elif action == "revert":
# Откатить версию может автор или менеджер
return user.has_perm("documents.change_document", obj)
# add_related/remove_related → проверяем django-rules change_document
if action in ("add_related", "remove_related"):
return user.has_perm("documents.change_document", obj)
# PUT/PATCH → проверяем django-rules change_document
if request.method in ("PUT", "PATCH"):
return user.has_perm("documents.change_document", obj)
# DELETE → проверяем django-rules delete_document
if request.method == "DELETE":
return user.has_perm("documents.delete_document", obj)
# SAFE/ack/download без модельных прав → только получатели или sent_to_all
if request.method in SAFE_METHODS or action in ("acknowledge", "download"):
if getattr(obj, "sent_to_all", False):
return True
return obj.recipients.filter(pk=user.pk, is_active=True).exists()
# Небезопасные без прав — нельзя (POST custom actions)
return False