153 lines
5.8 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.

# backend\api\v1\permissions.py
from __future__ import annotations
from typing import Any, Iterable, Optional
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.request import Request
class AdminOrActionOrModelPerms(DjangoModelPermissions):
"""staff/superuser ИЛИ явные пермы экшена ИЛИ стандартные
DjangoModelPermissions.
Логика:
1) staff/superuser → True
2) Если на вью заданы «явные» коды прав для текущего action
(метод `_explicit_perm_codes`), достаточно ЛЮБОГО из них
→ True/False
3) Иначе — делегируем в DjangoModelPermissions.has_permission():
- для SAFE_METHODS потребует `view_<model>`
- для небезопасных — коды из perms_map (add/change/delete)
"""
perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [],
"HEAD": [],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}
@staticmethod
def _normalize_codes(value: Any) -> Optional[list[str]]:
"""Приводит конфигурацию кодов прав к списку строк.
Args:
value (Any): Значение из конфигурации прав:
- str — один код,
- Iterable[str] — несколько кодов,
- None — отсутствует.
Returns:
Optional[list[str]]: Список кодов или None.
Raises:
TypeError: Если найден нестроковый элемент в Iterable.
"""
if value is None:
return None
if isinstance(value, str):
v = value.strip()
return [v] if v else None
if isinstance(value, Iterable):
out: list[str] = []
for v in value:
if not isinstance(v, str):
raise TypeError("Коды прав должны быть строками.")
v = v.strip()
if v:
out.append(v)
return out or None
return None
@staticmethod
def _explicit_perm_codes(view) -> Optional[list[str]]:
"""Достаёт из вью «явные» коды прав (ИЛИ между кодами).
Поддерживаемые формы на ViewSet:
- required_perm_code: str | Iterable[str]
- required_perms_by_action[action]: str | Iterable[str]
- required_perms_by_action[action]: {method: str|Iterable[str]}
где method сравнивается без учёта регистра,
возможны '*', 'ANY', а HEAD трактуется как GET.
Args:
view: DRF ViewSet с полями `action`, `request` (опционально).
Returns:
Optional[list[str]]: Список кодов прав или None.
"""
# 1) Единый код(ы) на всю вью
codes = AdminOrActionOrModelPerms._normalize_codes(
getattr(view, "required_perm_code", None)
)
if codes:
return codes
# 2) Карта по экшенам (+ по методам)
mapping = getattr(view, "required_perms_by_action", None) or {}
if not isinstance(mapping, dict):
return None
action = getattr(view, "action", None)
if not action or action not in mapping:
return None
value = mapping[action]
# 2.1) Прямо строка/итерируемое — сразу нормализуем
codes = AdminOrActionOrModelPerms._normalize_codes(value)
if codes is not None:
return codes
# 2.2) Вложенная карта по методам
if isinstance(value, dict):
method = getattr(getattr(view, "request", None), "method", None)
if not method:
return None
m = method.upper()
keys = (m, m.lower())
if m == "HEAD":
keys += ("GET", "get")
keys += ("*", "ANY")
for k in keys:
if k in value:
return AdminOrActionOrModelPerms._normalize_codes(value[k])
return None
# Иное — игнорируем
return None
def has_permission(self, request: Request, view) -> bool:
"""Проверяет доступ на уровне запроса.
Args:
request (Request): DRF-запрос.
view: DRF View/ViewSet.
Returns:
bool: Разрешён ли доступ.
Raises:
PermissionDenied: не выбрасывается здесь; при False DRF отдаст 403.
"""
user = getattr(request, "user", None)
if not (user and user.is_authenticated):
return False
if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False):
return True
# 1) Явные пермы, привязанные к action.
# Если у тебя есть такой механизм.
explicit: Optional[Iterable[str]] = getattr(
self, "_explicit_perm_codes", lambda _v: None
)(view)
if explicit:
return any(user.has_perm(code) for code in explicit)
# 2) Всё остальное — родителю (в т.ч. SAFE_METHODS → view_<model>)
return super().has_permission(request, view)