Corp/backend/api/v1/requests_app/permissions.py

231 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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