feat: проверка что только получатель заявки может принять решение

- Добавлен IsRecipientOfRequest permission для API
- Обновлен get_allowed_request_qs: теперь включает заявки где пользователь - получатель
- Добавлена проверка recipients в request_process view
- Staff/superuser могут обрабатывать любые заявки (обход проверки)
This commit is contained in:
Игорь Надеин 2025-12-29 10:36:56 +03:00
parent 66e5279850
commit aa6fa29e3a
3 changed files with 72 additions and 6 deletions

View File

@ -10,6 +10,33 @@ from employees.constants import DeptPerm
from employees.models import Department
class IsRecipientOfRequest(BasePermission):
"""Проверяет, что пользователь является получателем заявки.
Доступ разрешается только если пользователь указан в recipients
данной заявки. Это базовая проверка которая должна комбинироваться
с проверкой прав (AdminOrActionOrModelPerms или DeptCanProcess).
Для staff/superuser проверка пропускается (они могут обрабатывать любые заявки).
"""
message = "Вы не являетесь получателем этой заявки."
def has_permission(self, request: Request, view: Any) -> bool:
return bool(getattr(request.user, "is_authenticated", False))
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
user = request.user
# Staff/superuser могут обрабатывать любые заявки
if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False):
return True
# Проверяем, является ли пользователь получателем заявки
# (только recipients, не cc_users - люди в копии не принимают решения)
return obj.recipients.filter(id=user.id).exists()
class CommentsPermission(BasePermission):
"""Комментарии: модельные права, staff, head.

View File

@ -32,6 +32,7 @@ from .permissions import (
DeptChangeRequest,
DeptComments,
DeptViewRequest,
IsRecipientOfRequest,
NotFinalOrStaff,
)
from .serializers import (
@ -86,7 +87,12 @@ class RequestViewSet(viewsets.ModelViewSet):
if self.action == "comments":
return [(CommentsPermission | DeptComments)()]
if self.action in {"approve", "reject"}:
return [(AdminOrActionOrModelPerms | DeptCanProcess)()]
# Права на обработку + обязательно должен быть получателем заявки
# (staff/superuser пропускаются в IsRecipientOfRequest)
return [
(AdminOrActionOrModelPerms | DeptCanProcess)(),
IsRecipientOfRequest(),
]
if self.action == "cancel":
return [IsSelfOrStaff()]
if self.action == "retrieve":

View File

@ -144,13 +144,42 @@ def is_hr_or_head(user) -> bool:
return is_hr(user) or is_department_head(user)
def can_process_requests(user) -> bool:
"""Проверяет, может ли пользователь обрабатывать заявки вообще.
True если: HR/staff, руководитель отдела, или есть модельное право.
"""
if is_hr(user):
return True
if is_department_head(user):
return True
if user.has_perm('requests_app.can_process_requests'):
return True
return False
def get_allowed_request_qs(user):
"""HR — все; руководитель — заявки сотрудников своих отделов."""
"""Возвращает заявки, которые пользователь может обрабатывать.
HR/staff — все заявки;
Руководитель отдела — заявки сотрудников своих отделов;
Получатель — заявки, где он указан как recipient.
Финальная проверка в request_process дополнительно проверяет,
что пользователь является получателем конкретной заявки.
"""
if is_hr(user):
return BASE_REQUEST_QS
return BASE_REQUEST_QS.filter(
employee__departments_links__department__head=user
).distinct()
from django.db.models import Q
# Заявки от сотрудников отделов, где пользователь руководитель
dept_head_qs = Q(employee__departments_links__department__head=user)
# Заявки где пользователь указан как получатель
recipient_qs = Q(recipients=user)
return BASE_REQUEST_QS.filter(dept_head_qs | recipient_qs).distinct()
# ---------- Утилиты ----------
@ -311,7 +340,7 @@ def all_requests(request):
)
@user_passes_test(is_hr_or_head)
@user_passes_test(can_process_requests)
@require_http_methods(["GET", "POST"])
def request_process(request, pk):
allowed_qs = get_allowed_request_qs(request.user)
@ -320,6 +349,10 @@ def request_process(request, pk):
with transaction.atomic():
req = get_object_or_404(allowed_qs.select_for_update(), pk=pk)
req.refresh_from_db()
# Проверка: не-staff должен быть получателем заявки
if not is_hr(request.user) and not req.recipients.filter(id=request.user.id).exists():
raise PermissionDenied("Вы не являетесь получателем этой заявки.")
post = request.POST.copy()
# Фоллбек: если status отсутствует (или кнопка без JS), используем имя кнопки