231 lines
10 KiB
Python
231 lines
10 KiB
Python
# api/v1/requests_app/permissions.py
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Optional
|
||
|
||
from api.v1.permissions import AdminOrDeptAllowed
|
||
from rest_framework.permissions import BasePermission
|
||
from rest_framework.request import Request
|
||
from employees.constants import DeptPerm
|
||
from employees.models import Department
|
||
|
||
|
||
class CommentsPermission(BasePermission):
|
||
"""Комментарии: модельные права, staff, head.
|
||
|
||
GET/HEAD: view_requestcomment OR staff OR head (прямой/по сотруднику).
|
||
POST: add_requestcomment OR staff OR head (прямой/по сотруднику).
|
||
"""
|
||
|
||
message = "Недостаточно прав для доступа к комментариям."
|
||
|
||
def _required_perm(self, request: Request) -> Optional[str]:
|
||
if request.method in {"GET", "HEAD"}:
|
||
return "requests_app.view_requestcomment"
|
||
if request.method == "POST":
|
||
return "requests_app.add_requestcomment"
|
||
return None
|
||
|
||
def has_permission(self, request: Request, view: Any) -> bool:
|
||
return bool(getattr(request.user, "is_authenticated", False))
|
||
|
||
def _is_head_for_request(self, user, obj) -> bool:
|
||
dept = getattr(obj, "department", None)
|
||
if dept is not None and getattr(dept, "head_id", None) == user.id:
|
||
return True
|
||
emp_id = getattr(obj, "employee_id", None)
|
||
if not emp_id:
|
||
return False
|
||
return Department.objects.filter(
|
||
head_id=user.id,
|
||
employeedepartment__employee_id=emp_id,
|
||
employeedepartment__is_active=True,
|
||
).exists()
|
||
|
||
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
|
||
user = request.user
|
||
if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False):
|
||
return True
|
||
if self._is_head_for_request(user, obj):
|
||
return True
|
||
code = self._required_perm(request)
|
||
return bool(code and user.has_perm(code))
|
||
|
||
|
||
class NotFinalOrStaff(BasePermission):
|
||
"""Запрещает удаление финальной заявки для не-админов.
|
||
|
||
- Разрешает любые методы, кроме DELETE, без ограничений.
|
||
- Для DELETE: разрешено, если заявка не финальная, либо пользователь — staff.
|
||
|
||
Атрибуты:
|
||
message (str): Текст ошибки для 403 Forbidden.
|
||
|
||
Notes:
|
||
Пермишены не выбрасывают исключения — они возвращают False,
|
||
что приводит к 403 Forbidden.
|
||
"""
|
||
|
||
message = "Финальная заявка не может быть удалена."
|
||
|
||
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
|
||
"""Объектная проверка.
|
||
|
||
Args:
|
||
request (Request): Текущий запрос DRF.
|
||
view (Any): Экземпляр вьюсета.
|
||
obj (Any): Экземпляр удаляемого объекта (заявки).
|
||
|
||
Returns:
|
||
bool: True, если доступ разрешён; иначе False.
|
||
"""
|
||
if request.method != "DELETE":
|
||
return True
|
||
is_final = bool(getattr(obj, "is_final", False))
|
||
is_staff = bool(getattr(request.user, "is_staff", False))
|
||
return (not is_final) or is_staff
|
||
|
||
|
||
class DeptViewRequest(AdminOrDeptAllowed):
|
||
"""Просмотр заявки по департаментным правам.
|
||
|
||
Доступ разрешается, если пользователь — staff/superuser ИЛИ имеет
|
||
право отдела ``view_request`` именно для отдела данной заявки.
|
||
|
||
Применяется только к detail-экшену `retrieve`.
|
||
|
||
Attributes:
|
||
allow_safe_without_code (bool): Запрещает фоллбек пропуска SAFE-методов
|
||
без явного кода права (защита от «случайных 200»).
|
||
required_code_map (dict[str, str]): Ограничиваем право только на `retrieve`.
|
||
required_code (Optional[str]): Явно отключаем общий fallback.
|
||
"""
|
||
|
||
# SAFE-методы не должны проходить «на шару»
|
||
allow_safe_without_code: bool = False
|
||
|
||
# Явно ограничиваем сферу действия права
|
||
required_code_map: dict[str, str] = {
|
||
"retrieve": "view_request",
|
||
}
|
||
|
||
# Не даём базовому классу брать «любой экшен = view_request»
|
||
required_code: Optional[str] = None
|
||
|
||
def get_required_code(self, request: Request, view: Any) -> Optional[str]:
|
||
"""Возвращает код департаментного права только для поддерживаемых action.
|
||
|
||
Args:
|
||
request (Request): DRF-запрос.
|
||
view (Any): Вью/вьюсет; важен `view.action`.
|
||
|
||
Returns:
|
||
Optional[str]: 'view_request' для `retrieve`, иначе None (запрет).
|
||
"""
|
||
return self.required_code_map.get(getattr(view, "action", None))
|
||
|
||
|
||
class DeptCanProcess(AdminOrDeptAllowed):
|
||
"""Пермишен для обработки заявок по департаментным правам.
|
||
|
||
Доступ разрешается, если пользователь — staff/superuser ИЛИ имеет
|
||
право отдела ``can_process_requests`` для отдела заявки.
|
||
|
||
Применяется на детальных экшенах workflow:
|
||
- approve
|
||
- reject
|
||
|
||
Атрибуты:
|
||
allow_safe_without_code (bool): Запретить «фоллбек» на SAFE-методы
|
||
без явного кода права (по умолчанию в базовом классе мог быть True).
|
||
required_code_map (dict[str, str]): Маппинг action → код права.
|
||
required_code (str): Резервный код права, если action не сматчился.
|
||
|
||
Raises:
|
||
Ничего напрямую не выбрасывает — при отсутствии прав вернёт False,
|
||
DRF ответит 403 Forbidden.
|
||
"""
|
||
|
||
# SAFE-методы пусть тоже требуют явного кода (на всякий случай)
|
||
allow_safe_without_code: bool = False
|
||
|
||
# Явно описываем нужные action-ы workflow
|
||
required_code_map: dict[str, str] = {
|
||
"approve": "can_process_requests",
|
||
"reject": "can_process_requests",
|
||
}
|
||
|
||
# Резервный вариант — если вдруг action неизвестен,
|
||
# базовый класс возьмёт этот код.
|
||
required_code: str = "can_process_requests"
|
||
|
||
|
||
class DeptChangeRequest(AdminOrDeptAllowed):
|
||
"""Правка заявок по департаментным правам.
|
||
|
||
Доступ разрешается, если пользователь — staff/superuser ИЛИ имеет
|
||
право отдела ``change_request`` для отдела конкретной заявки.
|
||
|
||
Применяется ТОЛЬКО к экшенам редактирования:
|
||
- update (PUT)
|
||
- partial_update (PATCH)
|
||
- destroy (DELETE)
|
||
|
||
ВНИМАНИЕ: для workflow-экшенов (approve/reject) используйте DeptCanProcess.
|
||
|
||
Attributes:
|
||
allow_safe_without_code (bool): запрещает фоллбек пропуска SAFE-методов
|
||
без явного кода права (защита от случайных «200»).
|
||
"""
|
||
|
||
# Не пропускаем SAFE-методы без явного кода (страховка)
|
||
allow_safe_without_code: bool = False
|
||
|
||
def get_required_code(self, request: Request, view: Any) -> Optional[str]:
|
||
"""Возвращает код департаментного права для экшенов редактирования.
|
||
|
||
Args:
|
||
request (Request): DRF-запрос.
|
||
view (Any): Вью/вьюсет (важен view.action).
|
||
|
||
Returns:
|
||
Optional[str]: 'change_request' для update/partial_update/destroy, иначе None.
|
||
|
||
Notes:
|
||
Возврат None означает «кода нет» → базовый класс вернёт False (403),
|
||
так мы исключаем approve/reject и прочие экшены из этого пермишена.
|
||
"""
|
||
if getattr(view, "action", None) in {"update", "partial_update", "destroy"}:
|
||
return "change_request"
|
||
return None
|
||
|
||
|
||
class DeptComments(AdminOrDeptAllowed):
|
||
"""Комментарии: staff ИЛИ департаментное право.
|
||
|
||
- GET/HEAD → 'view_requestcomment'
|
||
- POST → 'add_requestcomment'
|
||
"""
|
||
|
||
# Чтобы SAFE-методы не проходили «без кода» при ошибке конфигурации:
|
||
allow_safe_without_code = False
|
||
|
||
def get_required_code(self, request: Request, view: Any) -> Optional[str]:
|
||
"""Возвращает код департаментного права для экшена `comments`.
|
||
|
||
Args:
|
||
request (Request): Текущий запрос DRF.
|
||
view (Any): Вью/вьюсет (не используется).
|
||
|
||
Returns:
|
||
Optional[str]: Код права для текущего HTTP-метода или None для неподдерживаемых методов.
|
||
|
||
Raises:
|
||
None: Исключения не выбрасываются — при отсутствии кода доступ будет запрещён базовым классом.
|
||
"""
|
||
if request.method in {"GET", "HEAD"}:
|
||
return DeptPerm.VIEW_REQUESTCOMMENT
|
||
if request.method == "POST":
|
||
return DeptPerm.ADD_REQUESTCOMMENT
|
||
return None
|