# 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_` - для небезопасных — коды из 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_) return super().has_permission(request, view)