153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
# 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)
|