# backend\api\v1\permissions.py from __future__ import annotations from typing import Any, Dict, Optional from django.contrib.auth.models import AbstractBaseUser from rest_framework.permissions import SAFE_METHODS, BasePermission from employees.models import Department, EmployeeDepartment, RoleAssignment MANAGE_PERM = "manage_department" CHANGE_HEAD_PERM = "change_department_head" ASSIGN_ROLE_PERM = "assign_department_role" def user_is_dept_head(user, dept: Department) -> bool: return bool( user and user.is_authenticated and dept.head_id == getattr(user, "id", None) ) def user_is_staffish(user) -> bool: return bool(user and user.is_authenticated and (user.is_superuser or user.is_staff)) def user_has_dept_perm(user, dept: Department, perm_code: str) -> bool: """Проверяет, есть ли у пользователя право `perm_code` в указанном отделе. Правила: - Неаутентифицированным → False. - staff/superuser → True. - Руководитель отдела → True. - Иначе: проверяет RoleAssignment — есть ли активное назначение роли с нужным правом в данном отделе. Args: user (User): Текущий пользователь. dept (Department): Отдел. perm_code (str): Код департаментного права (см. DeptPerm.CHOICES). Returns: bool: True, если доступ разрешён. Raises: ValueError: Если `dept` не задан или не имеет id. """ if not (user and user.is_authenticated): return False # staff/superuser — сразу ок if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False): return True if not isinstance(dept, Department) or getattr(dept, "id", None) is None: raise ValueError("Argument `dept` must be Department instance with a valid id.") # руководитель — сразу ок if getattr(dept, "head_id", None) == user.id: return True # Проверка через RoleAssignment (новая модель — не требует членства в # отделе) if RoleAssignment.objects.filter( employee_id=user.id, role__department_id=dept.id, role__scoped_permissions__code=perm_code, is_active=True, ).exists(): return True # Fallback: старая проверка через EmployeeDepartment.role (обратная # совместимость) return EmployeeDepartment.objects.filter( employee_id=user.id, department_id=dept.id, is_active=True, role__scoped_permissions__code=perm_code, ).exists() def has_dept_perm(user: AbstractBaseUser, department_id: int, code: str) -> bool: """ Возвращает True, если пользователь имеет право `code` в рамках отдела `department_id`. Логика: 1) staff/superuser → всегда True 2) пользователь является текущим руководителем отдела → True для любых «управленческих» кодов 3) пользователь имеет активное назначение роли (RoleAssignment) с правом `code` → True 4) fallback: старая проверка через EmployeeDepartment.role 5) иначе False """ if not getattr(user, "is_authenticated", False): return False if getattr(user, "is_superuser", False) or getattr(user, "is_staff", False): return True # 2) Руководитель отдела имеет все управленческие права в своём отделе if Department.objects.filter(id=department_id, head_id=user.id).exists(): return True # 3) Проверка через RoleAssignment (новая модель — не требует членства в # отделе) if RoleAssignment.objects.filter( employee_id=user.id, role__department_id=department_id, role__scoped_permissions__code=code, is_active=True, ).exists(): return True # 4) Fallback: старая проверка через EmployeeDepartment.role link = ( EmployeeDepartment.objects.select_related("role") .filter(employee_id=user.id, department_id=department_id, is_active=True) .first() ) if not link or not link.role_id: return False return link.role.scoped_permissions.filter(code=code).exists() def can_publish_department_posts(user: AbstractBaseUser, department_id: int) -> bool: """Возвращает True, если пользователь может публиковать новости отдела. Упрощённый продуктовый контракт: - staff/superuser → всегда True - руководитель отдела → True - любой активный физический участник отдела → True - любой сотрудник с активной ролью в отделе → True """ if not getattr(user, "is_authenticated", False): return False if getattr(user, "is_superuser", False) or getattr(user, "is_staff", False): return True if Department.objects.filter(id=department_id, head_id=user.id).exists(): return True if EmployeeDepartment.objects.filter( employee_id=user.id, department_id=department_id, is_active=True, ).exists(): return True return RoleAssignment.objects.filter( employee_id=user.id, role__department_id=department_id, is_active=True, ).exists() class AdminOrDeptAllowed(BasePermission): """Комбинированный пермишен: админский доступ ИЛИ доступ по праву на конкретный отдел. Поведение: 1. Если пользователь superuser/staff — доступ всегда разрешён. 2. Если для текущего экшена требуется код права (например, DeptPerm.MANAGE_FEED), то доступ разрешается только при наличии у пользователя этого права на указанный отдел. 3. Идентификатор отдела извлекается из: - URL kwargs: 'department_pk' → 'department_id'; - request.data: 'department' → 'department_id'; - query params: 'department' → 'department_id'. На уровне has_object_permission отдел берётся из самого объекта: - Department.id; - obj.department_id / obj.dept_id; - obj.department.id. 4. Если код права не задан (required_code/required_code_map пусты), то для SAFE-методов (GET/HEAD/OPTIONS) доступ разрешён, если allow_safe_without_code=True (по умолчанию). Для небезопасных методов в этом случае доступ запрещён. Атрибуты класса: required_code (str|None): Единый код права для всех экшенов. Если None, используется required_code_map. required_code_map (dict[str,str]): Коды прав по имени action (create/update/partial_update/destroy/...). allow_safe_without_code (bool): Разрешать ли SAFE-методы, когда код не задан. По умолчанию True. Примеры: class ManageFeedPerm(AdminOrDeptAllowed): required_code = DeptPerm.MANAGE_FEED class DeptFeedPerm(AdminOrDeptAllowed): required_code_map = { "create": DeptPerm.MANAGE_FEED, "update": DeptPerm.MANAGE_FEED, "partial_update": DeptPerm.MANAGE_FEED, "destroy": DeptPerm.MANAGE_FEED, } """ required_code: Optional[str] = None required_code_map: Dict[str, str] = {} allow_safe_without_code: bool = True # -------- helpers -------- def get_required_code(self, request, view) -> Optional[str]: """Определяет требуемый код права для текущего экшена. Сначала берёт из required_code_map по имени action (DRF: view.action), затем fallback на required_code. Args: request: DRF Request (не используется в текущей логике). view: DRF View/ViewSet; важно наличие атрибута 'action' для ViewSet. Returns: Optional[str]: Код права или None, если право для экшена не требуется. """ return ( self.required_code_map.get(getattr(view, "action", None)) or self.required_code ) def _extract_dept_id_from_request(self, request, view) -> Optional[int]: """Пытается извлечь department_id из kwargs, body или query-параметров. Порядок: 1) view.kwargs: department_pk, department_id 2) request.data: department, department_id 3) request.query_params: department, department_id Args: request: DRF Request view: DRF View Returns: Optional[int]: Значение dept_id, если найдено и приведено к int, иначе None. """ for k in ("department_pk", "department_id"): v = view.kwargs.get(k) if v is not None: return v for k in ("department", "department_id"): v = request.data.get(k) if v is not None: return v for k in ("department", "department_id"): v = request.query_params.get(k) if v is not None: return v return None def _extract_dept_id_from_obj(self, obj: Any) -> Optional[int]: """Извлекает department_id из объекта. Порядок: - Если obj — модель Department с полем id → вернуть id. - Если у obj есть атрибуты department_id или dept_id → вернуть его. - Если у obj есть атрибут department (FK) → взять department.id. Args: obj: Модель/объект detail-view. Returns: Optional[int]: department_id или None. """ if obj is None: return None if obj.__class__.__name__ == "Department" and hasattr(obj, "id"): return obj.id for attr in ("department_id", "dept_id"): if hasattr(obj, attr): return getattr(obj, attr) dep = getattr(obj, "department", None) if dep is not None: return getattr(dep, "id", None) return None def _to_int_or_none(self, v) -> Optional[int]: """Аккуратное приведение к int. Args: v: Любое значение с возможностью строкового преобразования. Returns: Optional[int]: int или None при ошибке. """ try: return int(str(v)) except (TypeError, ValueError): return None # -------- main checks -------- def has_permission(self, request, view) -> bool: """См. докстринг класса. Если dept_id не найден, но это detail-запрос, откладываем проверку до object-level (вернём True). """ user = getattr(request, "user", None) if not user or not user.is_authenticated: return False if getattr(user, "is_superuser", False) or getattr(user, "is_staff", False): return True code = self.get_required_code(request, view) if not code: return ( (request.method in SAFE_METHODS) if self.allow_safe_without_code else False ) dept_id = self._to_int_or_none( self._extract_dept_id_from_request(request, view) ) if dept_id is None: # robust: считаем detail, если есть lookup в kwargs lookup_kw = getattr(view, "lookup_url_kwarg", None) or getattr( view, "lookup_field", "pk" ) is_detail = getattr(view, "detail", None) if is_detail is None: is_detail = lookup_kw in getattr(view, "kwargs", {}) if is_detail: # отложим проверку до has_object_permission return True return False return has_dept_perm(user, dept_id, code) def has_object_permission(self, request, view, obj) -> bool: """Проверка доступа к конкретному объекту (detail). Правила: - superuser/staff → True. - Если код не требуется → SAFE-методы допускаются, если allow_safe_without_code=True. - Иначе: - Пытаемся извлечь department_id из объекта; - если не удалось — пробуем извлечь из запроса (kwargs/data/query); - если не нашли вообще → False; - иначе → has_dept_perm(user, dept_id, code). Returns: bool: Разрешить/запретить доступ к объекту. """ user = getattr(request, "user", None) if getattr(user, "is_superuser", False) or getattr(user, "is_staff", False): return True code = self.get_required_code(request, view) if not code: return ( request.method in SAFE_METHODS if self.allow_safe_without_code else False ) dept_id = self._to_int_or_none(self._extract_dept_id_from_obj(obj)) if dept_id is None: dept_id = self._to_int_or_none( self._extract_dept_id_from_request(request, view) ) return has_dept_perm(request.user, dept_id, code)