Files
ACORE/core/tests_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

338 lines
16 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.
from datetime import date
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from apps.employees.models import Employee, EmployeePermissionOverride
from core.models import PortalAccess
from core.portal_access import DASHBOARD_PATH, user_can_access, user_can_manage_hr_portal
from core.accounting_portal import user_can_access_accounting_portal
def make_employee(**overrides):
n = Employee.objects.count() + 1
defaults = {
'username': f'portal.user{n}',
'email': f'portal.user{n}@company.local',
'first_name': 'Тест',
'last_name': 'Портал',
'hire_date': date(2026, 1, 1),
'user_location_type': 'pharmacy',
'mattermost_sync_enabled': False,
'ad_sync_enabled': False,
'is_staff': False,
}
defaults.update(overrides)
return Employee.objects.create(**defaults)
def assert_denied_to_dashboard(test, response, msg=None):
test.assertEqual(response.status_code, 302, msg)
test.assertEqual(response['Location'], DASHBOARD_PATH, msg)
if response.has_header('HX-Redirect'):
test.assertEqual(response['HX-Redirect'], DASHBOARD_PATH, msg)
from core.test_utils import login_2fa
def portal_perm(codename: str) -> Permission:
ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False)
return Permission.objects.get(content_type=ct, codename=codename)
class PortalAccessUnitTests(TestCase):
def test_baseline_sections_without_group(self):
user = make_employee()
# Базовые разделы видны даже без назначенных групп.
self.assertTrue(user_can_access(user, 'dashboard'))
self.assertTrue(user_can_access(user, 'profile'))
self.assertTrue(user_can_access(user, 'it_support'))
# Прочие «открытые» разделы теперь требуют хотя бы одной группы.
self.assertFalse(user_can_access(user, 'dislocation'))
self.assertFalse(user_can_access(user, 'news'))
self.assertFalse(user_can_access(user, 'tasks'))
self.assertFalse(user_can_access(user, 'help'))
self.assertFalse(user_can_access(user, 'rro'))
def test_open_sections_visible_with_any_group(self):
user = make_employee()
user.groups.add(Group.objects.create(name='Пустая роль'))
self.assertTrue(user_can_access(user, 'dislocation'))
self.assertTrue(user_can_access(user, 'news'))
self.assertTrue(user_can_access(user, 'tasks'))
self.assertTrue(user_can_access(user, 'help'))
def test_hr_portal_requires_group(self):
user = make_employee()
# Без группы HR-портал не виден.
self.assertFalse(user_can_access(user, 'hr_portal'))
self.assertFalse(user_can_manage_hr_portal(user))
user.groups.add(Group.objects.create(name='Пустая роль'))
# С группой раздел виден, но управление наймом всё ещё недоступно.
self.assertTrue(user_can_access(user, 'hr_portal'))
self.assertFalse(user_can_manage_hr_portal(user))
def test_accounting_portal_requires_subsection(self):
user = make_employee()
self.assertFalse(user_can_access_accounting_portal(user))
user.groups.add(Group.objects.get(name='Фармацевт'))
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
if hasattr(user, attr):
delattr(user, attr)
self.assertTrue(user_can_access_accounting_portal(user))
def test_office_has_internet_billing_perm(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
if hasattr(user, attr):
delattr(user, attr)
self.assertTrue(user_can_access(user, 'internet_billing'))
def test_restricted_denied_without_perm(self):
user = make_employee()
self.assertFalse(user_can_access(user, 'it_manager'))
def test_staff_without_perm_denied(self):
user = make_employee(is_staff=True)
self.assertFalse(user_can_manage_hr_portal(user))
self.assertFalse(user_can_access(user, 'databases'))
def test_hr_group_manage_perm(self):
user = make_employee()
hr = Group.objects.get(name='HR')
user.groups.add(hr)
# clear cached perms
if hasattr(user, '_perm_cache'):
del user._perm_cache
if hasattr(user, '_user_perm_cache'):
del user._user_perm_cache
if hasattr(user, '_group_perm_cache'):
del user._group_perm_cache
self.assertTrue(user_can_access(user, 'hr_portal'))
self.assertTrue(user_can_manage_hr_portal(user))
self.assertTrue(user_can_access(user, 'recruiting'))
self.assertFalse(user_can_access(user, 'it_manager'))
def test_override_grant(self):
user = make_employee()
EmployeePermissionOverride.objects.create(
employee=user,
permission=portal_perm('access_hr_portal'),
override_type='grant',
)
self.assertTrue(user_can_manage_hr_portal(user))
def test_override_revoke(self):
user = make_employee()
it = Group.objects.get(name='IT')
user.groups.add(it)
if hasattr(user, '_perm_cache'):
del user._perm_cache
self.assertTrue(user_can_access(user, 'it_manager'))
EmployeePermissionOverride.objects.create(
employee=user,
permission=portal_perm('access_it_manager'),
override_type='revoke',
)
self.assertFalse(user_can_access(user, 'it_manager'))
class PortalAccessViewTests(TestCase):
def test_pharmacist_open_ok_restricted_forbidden(self):
user = make_employee()
user.groups.add(Group.objects.create(name='Пустая роль'))
login_2fa(self.client, user)
self.assertEqual(self.client.get('/dashboard/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/dislocation/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/help/', HTTP_HX_REQUEST='true').status_code, 200)
assert_denied_to_dashboard(self, self.client.get('/rro/', HTTP_HX_REQUEST='true'))
assert_denied_to_dashboard(self, self.client.get('/pharmacies/', HTTP_HX_REQUEST='true'))
assert_denied_to_dashboard(self, self.client.get('/utilities/', HTTP_HX_REQUEST='true'))
self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 200)
assert_denied_to_dashboard(self, self.client.get('/it-manager/', HTTP_HX_REQUEST='true'))
assert_denied_to_dashboard(self, self.client.get('/databases/', HTTP_HX_REQUEST='true'))
def test_pharmacist_hr_portal_read_only(self):
user = make_employee()
user.groups.add(Group.objects.create(name='Пустая роль'))
login_2fa(self.client, user)
response = self.client.get('/hr-portal/', HTTP_HX_REQUEST='true')
self.assertEqual(response.status_code, 200)
self.assertFalse(response.context['can_manage_hiring'])
def test_pharmacist_cannot_create_hiring(self):
user = make_employee()
login_2fa(self.client, user)
response = self.client.post('/hiring/', {'last_name': 'Test'}, HTTP_HX_REQUEST='true')
self.assertEqual(response.status_code, 403)
def test_employees_list_redirects_to_hr_portal(self):
user = make_employee()
login_2fa(self.client, user)
response = self.client.get('/employees/')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/hr-portal/')
def test_hr_group_can_open_hr_portal(self):
user = make_employee()
user.groups.add(Group.objects.get(name='HR'))
login_2fa(self.client, user)
self.assertEqual(self.client.get('/hr-portal/', HTTP_HX_REQUEST='true').status_code, 200)
def test_it_group_can_open_it_manager(self):
user = make_employee()
user.groups.add(Group.objects.get(name='IT'))
login_2fa(self.client, user)
self.assertEqual(self.client.get('/it-manager/', HTTP_HX_REQUEST='true').status_code, 200)
def test_staff_without_perm_views_forbidden(self):
user = make_employee(is_staff=True, totp_enabled=True)
login_2fa(self.client, user)
assert_denied_to_dashboard(self, self.client.get('/databases/', HTTP_HX_REQUEST='true'))
assert_denied_to_dashboard(self, self.client.get('/ad-mapping/', HTTP_HX_REQUEST='true'))
def test_it_group_can_open_databases_and_ad(self):
user = make_employee(totp_enabled=True)
user.groups.add(Group.objects.get(name='IT'))
login_2fa(self.client, user)
self.assertEqual(self.client.get('/databases/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/ad-mapping/', HTTP_HX_REQUEST='true').status_code, 200)
def test_manage_sections_require_permission(self):
user = make_employee()
login_2fa(self.client, user)
for url in ('/pharmacies/', '/rro/', '/utilities/', '/structure/', '/timetrack/'):
assert_denied_to_dashboard(self, self.client.get(url, HTTP_HX_REQUEST='true'), url)
def test_office_group_manage_sections(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
login_2fa(self.client, user)
for url in ('/pharmacies/', '/utilities/', '/structure/', '/timetrack/', '/accounting/'):
self.assertEqual(self.client.get(url, HTTP_HX_REQUEST='true').status_code, 200, url)
assert_denied_to_dashboard(self, self.client.get('/rro/', HTTP_HX_REQUEST='true'))
# /recruiting/ всегда редиректит на вкладку вакансий HR
self.assertEqual(self.client.get('/recruiting/', HTTP_HX_REQUEST='true').status_code, 302)
def test_accounting_portal_forbidden_without_subsection_perm(self):
user = make_employee()
login_2fa(self.client, user)
assert_denied_to_dashboard(self, self.client.get('/accounting/', HTTP_HX_REQUEST='true'))
def test_accounting_portal_shows_available_tabs(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
login_2fa(self.client, user)
response = self.client.get('/accounting/', HTTP_HX_REQUEST='true')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['accounting_active_tab'], 'utilities')
tab_keys = [t[0] for t in response.context['accounting_tabs']]
self.assertNotIn('rro', tab_keys)
self.assertIn('utilities', tab_keys)
self.assertIn('power', tab_keys)
self.assertIn('timetrack', tab_keys)
def test_accounting_portal_tab_param(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
login_2fa(self.client, user)
response = self.client.get('/accounting/?tab=timetrack', HTTP_HX_REQUEST='true')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['accounting_active_tab'], 'timetrack')
def test_office_rro_via_override(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
EmployeePermissionOverride.objects.create(
employee=user,
permission=portal_perm('access_rro'),
override_type='grant',
)
login_2fa(self.client, user)
self.assertEqual(self.client.get('/rro/', HTTP_HX_REQUEST='true').status_code, 200)
def test_manager_group_rro_and_utilities(self):
user = make_employee(user_location_type='pharmacy')
user.groups.add(Group.objects.get(name='Заведующий аптекой'))
login_2fa(self.client, user)
self.assertEqual(self.client.get('/rro/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/utilities/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/pharmacies/', HTTP_HX_REQUEST='true').status_code, 200)
def test_stub_sections_staff_only(self):
user = make_employee()
user.groups.add(Group.objects.get(name='Офисный сотрудник'))
for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'):
if hasattr(user, attr):
delattr(user, attr)
self.assertFalse(user_can_access(user, 'purchases'))
self.assertTrue(user_can_access(user, 'help'))
staff = make_employee(is_staff=True, user_location_type='office')
self.assertTrue(user_can_access(staff, 'purchases'))
# help — обычный «открытый» раздел: без группы не виден даже staff.
self.assertFalse(user_can_access(staff, 'help'))
class NoGroupSectionAccessTests(TestCase):
"""Сотрудник без групп: только базовые разделы, прочее — редирект."""
def test_no_group_blocked_from_open_sections(self):
user = make_employee()
login_2fa(self.client, user)
# базовые — доступны
self.assertEqual(self.client.get('/dashboard/', HTTP_HX_REQUEST='true').status_code, 200)
self.assertEqual(self.client.get('/it-support/', HTTP_HX_REQUEST='true').status_code, 200)
# ранее открытые — теперь редирект на дашборд
for url in ('/news/', '/tasks/', '/documents/', '/chat/',
'/calendar/', '/dislocation/', '/hr-portal/', '/help/'):
assert_denied_to_dashboard(self, self.client.get(url, HTTP_HX_REQUEST='true'), url)
# /settings/ не блокируется — редирект на свой профиль (2FA/пароль).
settings_resp = self.client.get('/settings/')
self.assertEqual(settings_resp.status_code, 302)
self.assertIn(f'/employees/{user.pk}/?tab=settings', settings_resp['Location'])
def test_group_opens_sections(self):
user = make_employee()
user.groups.add(Group.objects.create(name='Пустая роль'))
login_2fa(self.client, user)
for url in ('/news/', '/tasks/', '/dislocation/', '/hr-portal/', '/help/'):
self.assertEqual(
self.client.get(url, HTTP_HX_REQUEST='true').status_code, 200, url,
)
class AccessPreviewTests(TestCase):
def test_preview_limits_superuser_like_pharmacist(self):
from django.test import override_settings
from core.access_preview import attach_preview_to_user
admin = make_employee(is_staff=True, is_superuser=True, user_location_type='office')
self.assertTrue(user_can_access(admin, 'it_manager'))
with override_settings(DEBUG=True):
attach_preview_to_user(admin, 'pharmacist')
self.assertFalse(user_can_access(admin, 'it_manager'))
self.assertFalse(user_can_access(admin, 'databases'))
self.assertTrue(user_can_access(admin, 'dashboard'))
self.assertTrue(user_can_access(admin, 'utilities'))
attach_preview_to_user(admin, '')
self.assertTrue(user_can_access(admin, 'it_manager'))
def test_preview_endpoint_sets_session(self):
from django.test import override_settings
admin = make_employee(is_staff=True, is_superuser=True, user_location_type='office')
with override_settings(DEBUG=True):
login_2fa(self.client, admin)
resp = self.client.post('/dev/access-preview/', {'preview': 'it', 'next': '/dashboard/'})
self.assertEqual(resp.status_code, 302)
self.assertEqual(self.client.session.get('dev_access_preview'), 'it')
# В preview staff-only заглушки недоступны → редирект на главную
assert_denied_to_dashboard(
self, self.client.get('/purchases/', HTTP_HX_REQUEST='true'),
)