377 lines
15 KiB
Python
Raw Permalink 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, 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)