b2f0c2a1e2
Ранее разделы с perm=None (Новости, Задачи, Дислокация, Документы, Чат, Календарь, HR-портал, Справка) были доступны любому вошедшему — сотрудник без назначенной группы видел почти весь портал. Теперь такие разделы требуют хотя бы одной группы; без групп доступны только базовые: Главная, Профиль, Поддержка IT. - user_can_access: BASELINE_OPEN_KEYS + gate «есть ли группа» для perm=None - PortalSectionAccessMiddleware: барьер по префиксу URL (в т.ч. вложенные страницы вроде /tasks/5/), /settings/ не гейтим (редирект на свой профиль) - index.html: пункты меню под can_section (сайдбар, дропдаун, bottom-nav), секция «Управление» скрывается целиком, если пусто - тесты: политика без-группы + прямой заход по URL; тест-фабрики выдают группу (реальный сотрудник её имеет) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
209 lines
7.6 KiB
Python
209 lines
7.6 KiB
Python
"""Проверка доступа к разделам портала + декоратор для views."""
|
||
from __future__ import annotations
|
||
|
||
from functools import wraps
|
||
|
||
from django.contrib.auth.views import redirect_to_login
|
||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||
|
||
from core.portal_sections import RESTRICTED_KEYS, STUB_SECTION_KEYS, get_section
|
||
|
||
HR_PORTAL_MANAGE_PERM = 'core.access_hr_portal'
|
||
SECTION_ACTIONS = frozenset({'create', 'update', 'delete'})
|
||
DASHBOARD_PATH = '/dashboard/'
|
||
|
||
# Staff-only разделы конфигурации (не в access-preview чужих ролей).
|
||
STAFF_ONLY_SECTION_KEYS = frozenset({
|
||
'user_groups',
|
||
}) | STUB_SECTION_KEYS
|
||
|
||
# Разделы, доступные любому вошедшему сотруднику даже без назначенных групп
|
||
# (непровизионированный сотрудник: посадочная, свой профиль, связь с IT).
|
||
# Всё остальное скрыто, пока сотруднику не назначат хотя бы одну группу.
|
||
BASELINE_OPEN_KEYS = frozenset({
|
||
'dashboard',
|
||
'profile',
|
||
'it_support',
|
||
})
|
||
|
||
|
||
def _user_has_any_group(user) -> bool:
|
||
"""Есть ли у пользователя хотя бы одна группа (роль)."""
|
||
if getattr(user, 'is_superuser', False):
|
||
return True
|
||
return user.groups.exists()
|
||
|
||
|
||
def _preview_has_group(user) -> bool:
|
||
"""В access-preview: у выбранного пресета есть хотя бы одна группа."""
|
||
return bool(getattr(user, '_access_preview_groups', None))
|
||
|
||
|
||
def deny_section_access(request):
|
||
"""Нет доступа к разделу — перенаправление на главную.
|
||
|
||
Для HTMX дополнительно ``HX-Redirect``, чтобы обновить URL в браузере.
|
||
"""
|
||
response = HttpResponseRedirect(DASHBOARD_PATH)
|
||
if request.headers.get('HX-Request'):
|
||
response['HX-Redirect'] = DASHBOARD_PATH
|
||
return response
|
||
|
||
|
||
def user_can_manage_hr_portal(user) -> bool:
|
||
"""Кадровые операции: найм, испытание, заявки HR."""
|
||
if not user or not getattr(user, 'is_authenticated', False):
|
||
return False
|
||
preview_perms = getattr(user, '_access_preview_perms', None)
|
||
if preview_perms is not None:
|
||
return HR_PORTAL_MANAGE_PERM in preview_perms
|
||
if getattr(user, 'is_superuser', False):
|
||
return True
|
||
return user.has_perm(HR_PORTAL_MANAGE_PERM)
|
||
|
||
|
||
def user_can_access(user, key: str) -> bool:
|
||
"""Может ли пользователь открыть раздел портала ``key``.
|
||
|
||
- не аутентифицирован → False
|
||
- stub / staff-only разделы → только staff, скрыты в access-preview
|
||
- активен DEBUG access-preview → права пресета (без superuser bypass)
|
||
- superuser → True
|
||
- раздел без perm → True (любой залогиненный)
|
||
- иначе user.has_perm(section.perm) (группы / overrides)
|
||
"""
|
||
if not user or not getattr(user, 'is_authenticated', False):
|
||
return False
|
||
|
||
section = get_section(key)
|
||
if section is None:
|
||
return False
|
||
|
||
preview_perms = getattr(user, '_access_preview_perms', None)
|
||
|
||
if key in STAFF_ONLY_SECTION_KEYS:
|
||
if preview_perms is not None:
|
||
return False
|
||
if not (getattr(user, 'is_staff', False) or getattr(user, 'is_superuser', False)):
|
||
return False
|
||
return True
|
||
|
||
# Базовые разделы видны всегда (в т.ч. без групп и в access-preview).
|
||
if key in BASELINE_OPEN_KEYS:
|
||
return True
|
||
|
||
if preview_perms is not None:
|
||
if section.perm is None:
|
||
return _preview_has_group(user)
|
||
return section.perm in preview_perms
|
||
|
||
if getattr(user, 'is_superuser', False):
|
||
return True
|
||
if section.perm is None:
|
||
# Открытый раздел, но без назначенной группы сотрудник его не видит.
|
||
return _user_has_any_group(user)
|
||
return user.has_perm(section.perm)
|
||
|
||
|
||
def user_can_section_action(user, key: str, action: str) -> bool:
|
||
"""Может ли пользователь выполнить create/update/delete в разделе ``key``.
|
||
|
||
Просмотр раздела обязателен (для hr_portal — manage через access_hr_portal).
|
||
Staff **не** обходит проверку автоматически — только superuser.
|
||
Если у группы есть view, но нет строки GroupSectionAccess — CUD=True (совместимость).
|
||
В access-preview: CUD = есть ли access_* в пресете.
|
||
|
||
Флаги CUD для большинства разделов пока только хранятся; enforcement — точечно
|
||
(например системные справочники).
|
||
"""
|
||
if action not in SECTION_ACTIONS:
|
||
return False
|
||
if not user or not getattr(user, 'is_authenticated', False):
|
||
return False
|
||
|
||
preview_perms = getattr(user, '_access_preview_perms', None)
|
||
if preview_perms is not None:
|
||
section = get_section(key)
|
||
if section is None:
|
||
return False
|
||
if key == 'hr_portal':
|
||
return HR_PORTAL_MANAGE_PERM in preview_perms
|
||
if section.perm is None:
|
||
return True
|
||
return section.perm in preview_perms
|
||
|
||
if getattr(user, 'is_superuser', False):
|
||
return True
|
||
|
||
if key == 'hr_portal':
|
||
if not user_can_manage_hr_portal(user):
|
||
return False
|
||
elif not user_can_access(user, key):
|
||
return False
|
||
|
||
from apps.employees.models import GroupSectionAccess
|
||
|
||
group_ids = list(user.groups.values_list('pk', flat=True))
|
||
if not group_ids:
|
||
return False
|
||
|
||
rows = list(
|
||
GroupSectionAccess.objects.filter(group_id__in=group_ids, section_key=key)
|
||
)
|
||
if not rows:
|
||
return True
|
||
|
||
attr = {
|
||
'create': 'can_create',
|
||
'update': 'can_update',
|
||
'delete': 'can_delete',
|
||
}[action]
|
||
return any(getattr(r, attr) for r in rows)
|
||
|
||
|
||
def section_required(key: str):
|
||
"""Декоратор: доступ к view только при user_can_access(key)."""
|
||
|
||
def decorator(view_func):
|
||
@wraps(view_func)
|
||
def _wrapped(request, *args, **kwargs):
|
||
user = request.user
|
||
if not user.is_authenticated:
|
||
return redirect_to_login(request.get_full_path())
|
||
if not user_can_access(user, key):
|
||
return deny_section_access(request)
|
||
return view_func(request, *args, **kwargs)
|
||
|
||
return _wrapped
|
||
|
||
return decorator
|
||
|
||
|
||
def hr_manage_required(view_func):
|
||
"""Доступ только при праве управления HR (найм, утверждение и т.д.)."""
|
||
|
||
@wraps(view_func)
|
||
def _wrapped(request, *args, **kwargs):
|
||
user = request.user
|
||
if not user.is_authenticated:
|
||
return redirect_to_login(request.get_full_path())
|
||
if not user_can_manage_hr_portal(user):
|
||
return HttpResponseForbidden('Нет доступа к управлению HR.')
|
||
return view_func(request, *args, **kwargs)
|
||
|
||
return _wrapped
|
||
|
||
|
||
def matrix_section_keys() -> tuple[str, ...]:
|
||
"""Ключи строк сводной таблицы доступа."""
|
||
return tuple(RESTRICTED_KEYS)
|
||
|
||
|
||
def matrix_section_label(key: str) -> str:
|
||
section = get_section(key)
|
||
if key == 'hr_portal':
|
||
return 'HR-портал (управление)'
|
||
if section:
|
||
return section.label
|
||
return key
|