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), используем имя кнопки