From aa6fa29e3a577d90deb7063b6abf1d0bd3876b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 29 Dec 2025 10:36:56 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=87=D1=82=D0=BE=20=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=D1=82=20=D0=BF=D1=80=D0=B8=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен IsRecipientOfRequest permission для API - Обновлен get_allowed_request_qs: теперь включает заявки где пользователь - получатель - Добавлена проверка recipients в request_process view - Staff/superuser могут обрабатывать любые заявки (обход проверки) --- backend/api/v1/requests_app/permissions.py | 27 ++++++++++++++ backend/api/v1/requests_app/views.py | 8 +++- backend/requests_app/views.py | 43 +++++++++++++++++++--- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/backend/api/v1/requests_app/permissions.py b/backend/api/v1/requests_app/permissions.py index 2d07fc3b..87a044a3 100644 --- a/backend/api/v1/requests_app/permissions.py +++ b/backend/api/v1/requests_app/permissions.py @@ -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. diff --git a/backend/api/v1/requests_app/views.py b/backend/api/v1/requests_app/views.py index 3bf3d665..0348cd2e 100644 --- a/backend/api/v1/requests_app/views.py +++ b/backend/api/v1/requests_app/views.py @@ -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": diff --git a/backend/requests_app/views.py b/backend/requests_app/views.py index 5a482796..396471c3 100644 --- a/backend/requests_app/views.py +++ b/backend/requests_app/views.py @@ -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), используем имя кнопки