377 lines
15 KiB
Python
377 lines
15 KiB
Python
# 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)
|