Files
ACORE/core/portal_access.py
T
lokses b2f0c2a1e2 core/portal: сотрудник без группы видит только базовые разделы
Ранее разделы с 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>
2026-09-21 18:26:15 +03:00

209 lines
7.6 KiB
Python
Raw 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.
"""Проверка доступа к разделам портала + декоратор для 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