Corp/backend/docs/testing/POSITION_PERMISSIONS_TESTING.md
Игорь Надеин 48631f43fd Реорганизация документации и скриптов проекта
- Создана структура docs/ с категориями: completed, guides, architecture, diagnostic, in-progress, reports
- Перемещено 47+ MD файлов из корня проекта в docs/
- Удалено 6 устаревших/дублирующихся файлов
- Создана структура backend/scripts/ для тестов и утилит
- Перемещено 35 Python скриптов в backend/scripts/
- Создана структура backend/docs/ для технической документации
- Перемещено 12 MD файлов backend в backend/docs/
- Перемещены LDAP тестовые данные в backend/tests/fixtures/
- Добавлены README файлы для навигации
- Добавлены правила в .github/copilot-instructions.md для размещения документации

Результат:
- Корень проекта: только README.md
- Корень backend/: только manage.py и Django приложения
- Вся документация организована в структурированные папки
2025-12-26 13:49:51 +03:00

29 KiB
Raw Blame History

Тестирование Прав Доступа Через Должности (Position Permissions)

✅ РЕШЕНИЕ НАЙДЕНО И ПРИМЕНЕНО

Дата: 8 декабря 2025
Статус: ✅ Исправлено и протестировано

Корневая Причина

В шаблоне feed/feed_list.html проверялись несуществующие права:

{% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}

Эти права никогда не создавались в Django! Правильное право для создания постов:

{% if user.is_staff or perms.feed.add_post %}

Диагностика Production

Запущен отладочный скрипт для пользователя maze3290@gmail.com:

User: maze3290@gmail.com
Position: СММ специалист
Position Groups: ['Дизайн СММ Реклама']
Group Permissions: ['add_post', 'change_post', 'delete_post', 'view_post', ...]

User Permissions (via PositionRoleBackend):
{'feed.add_post', 'feed.change_post', 'feed.delete_post', ...}

Has feed.publish_company_post: False  # ❌ Несуществующее право!
Has feed.add_post: True               # ✅ Реальное право!

Вывод: PositionRoleBackend работает правильно, проблема была в шаблоне!

Исправление

Файл: backend/templates/feed/feed_list.html (строка 19)

- {% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
+ {% if user.is_staff or perms.feed.add_post %}

Коммиты

  1. 769f2ed - test: add comprehensive position permissions tests
  2. 66406d9 - fix: add is_active check to PositionRoleBackend
  3. 4a6b92f - docs: add production debugging guide
  4. 6162d9f - fix: correct permission check in feed_list.html ← РЕШЕНИЕ

Описание Проблемы (Оригинал)

Исходная Жалоба Пользователя

"Ты говорил, что пользователь с должностью получает все права которые есть у групп в этой должности, но по факту кнопка создания публикаций не появляется"

Ожидаемое поведение:

  • Пользователь имеет должность (Position)
  • Должность связана с группами (Position.groups)
  • Группы имеют права (Group.permissions)
  • Пользователь должен получить все права из групп своей должности

Фактическое поведение:

  • UI кнопки не появляются (например, "Создать публикацию")
  • Пользователи с должностями не могут выполнять действия, несмотря на наличие прав в группах

Архитектура Системы Прав

Цепочка Наследования

Position → Position.groups (ManyToMany) → Group.permissions (ManyToMany) → Permission
                ↓
           Employee.position (ForeignKey)
                ↓
         User (связан через OneToOne)

Реализация Backend

Файл: backend/eusrr_backend/auth_backends.py

class PositionRoleBackend(BaseBackend):
    """
    Custom authentication backend that grants permissions based on 
    Position.groups relationships.
    """
    
    def get_all_permissions(self, user_obj, obj=None):
        if not user_obj.is_active or user_obj.is_anonymous:
            return set()
        
        # Получаем права из должности
        if hasattr(user_obj, 'employee') and user_obj.employee.position:
            pos = user_obj.employee.position
            return set(
                Permission.objects.filter(
                    group__positions=pos
                ).select_related('content_type').values_list(
                    'content_type__app_label',
                    'codename'
                )
            )
        
        return set()

Конфигурация: settings.py (строка 329)

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'eusrr_backend.auth_backends.LDAPAuthBackend',
    'eusrr_backend.auth_backends.LDAPGroupAuthBackend',
    'eusrr_backend.auth_backends.PositionRoleBackend',  # ← Наш backend
]

Модели

Файл: backend/employees/models.py

class Position(models.Model):
    """Должность сотрудника"""
    name = models.CharField(max_length=100, unique=True)
    groups = models.ManyToManyField(
        Group,
        related_name='positions',
        blank=True
    )

class Employee(models.Model):
    """Сотрудник"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    position = models.ForeignKey(
        Position,
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )

Тестовая Стратегия

Цель Тестов

Определить, является ли проблема:

  1. Логической ошибкой в PositionRoleBackend → Тесты упадут
  2. Проблемой конфигурации (кэширование, LDAP sync, deployment) → Тесты пройдут

Структура Тестов

Файл: backend/tests/employees/test_position_permissions.py

Базовые Тесты (TestPositionPermissions)

class TestPositionPermissions:
    """Основная функциональность"""
    
    ✅ test_user_without_position_has_no_permissions
       Проверяет: Пользователь без должности = нет прав
    
    ❌ test_user_with_position_gets_permissions
       Проверяет: Position с Group → User получает права
       
    ❌ test_position_change_updates_permissions
       Проверяет: Смена должности → права обновляются
       
    ❌ test_remove_position_removes_permissions
       Проверяет: Удаление должности → права убираются
       
    ❌ test_multiple_users_same_position
       Проверяет: Несколько user с одной Position → одинаковые права
       
    ❌ test_staff_user_keeps_permissions
       Проверяет: Staff users сохраняют is_staff права
       
    ✅ test_position_group_permissions_are_additive
       Проверяет: Несколько групп → права складываются
       
    ❌ test_get_all_permissions_includes_position_perms
       Проверяет: get_all_permissions() возвращает права должности

Граничные Случаи (TestPositionPermissionsEdgeCases)

class TestPositionPermissionsEdgeCases:
    """Edge cases и особые ситуации"""
    
    ✅ test_position_without_groups_grants_no_permissions
       Проверяет: Position без groups = нет прав
       
    ❌ test_inactive_user_has_no_permissions
       Проверяет: Неактивный user = нет прав (даже с Position)
       
    ❌ test_direct_user_permissions_and_position_permissions
       Проверяет: Прямые права user + права Position = аддитивны

Интеграционные Тесты (TestPositionPermissionsIntegration)

class TestPositionPermissionsIntegration:
    """Интеграция с реальными моделями"""
    
    ❌ test_feed_publish_permission_via_position
       Проверяет: Право "feed.publish_company_post" работает
       
    ❌ test_document_add_permission_via_position
       Проверяет: Право "documents.add_document" работает

Текущие Результаты

============== test session starts ==============
collected 13 items

✅ PASSED: 3 tests
❌ ERRORS: 10 tests

Passing Tests:
- test_user_without_position_has_no_permissions
- test_position_group_permissions_are_additive  
- test_position_without_groups_grants_no_permissions

Failing Tests:
- All tests with IntegrityError or fixture issues

Обнаруженные Проблемы в Тестах

Проблема 1: IntegrityError на Permission Creation

Ошибка:

IntegrityError: UNIQUE constraint failed: auth_permission.content_type_id, auth_permission.codename

Причина: Использование Permission.objects.get_or_create() в фикстурах приводит к попыткам создать дубликаты.

Решение: Изменить паттерн создания:

# ❌ Старый код
perm, _ = Permission.objects.get_or_create(
    codename='publish_company_post',
    content_type=feed_content_type,
    defaults={'name': 'Can publish company post'}
)

# ✅ Новый код  
perm = Permission.objects.filter(
    codename='publish_company_post',
    content_type=feed_content_type
).first()

if not perm:
    perm = Permission.objects.create(
        codename='publish_company_post',
        content_type=feed_content_type,
        name='Can publish company post'
    )

Статус: Исправлено в 3 местах:

  • ✅ Фикстура group_with_publish_perm (строки 44-62)
  • ✅ Фикстура group_with_document_perm (строки 64-82)
  • ✅ Тест test_position_group_permissions_are_additive (строки 244-269)

Проблема 2: Fixture Visibility Between Test Classes

Ошибка:

fixture 'engineer_position' not found
fixture 'feed_content_type' not found
fixture 'documents_content_type' not found

Причина: Фикстуры определены в TestPositionPermissions как методы класса, но дочерние классы TestPositionPermissionsEdgeCases и TestPositionPermissionsIntegration не имеют к ним доступа.

Возможные решения:

  1. Перенести фикстуры на уровень модуля (вне классов)
  2. Использовать @pytest.fixture(scope="module")
  3. Использовать наследование классов
  4. Создать фикстуры в conftest.py

Статус: ❌ Не исправлено

Проблема 3: Permission Objects Already Exist in Database

Ошибка: При запуске тестов Django уже имеет базовые права из миграций.

Решение: Использовать .filter().first() вместо .create() для всех Permission объектов.

Статус: ✅ Частично исправлено (нужна проверка всех 13 тестов)

План Исправления

Шаг 1: Рефакторинг Фикстур ✅ IN PROGRESS

  • Заменить get_or_create на filter().first() + conditional create
  • Применить в group_with_publish_perm
  • Применить в group_with_document_perm
  • Применить в test_position_group_permissions_are_additive
  • Перенести фикстуры на module level

Шаг 2: Запуск Тестов

cd /c/Users/Игорь/Dev/EUSRR/backend
pytest tests/employees/test_position_permissions.py -v --tb=short

Ожидаемый результат после исправлений:

  • Все 13 тестов должны запуститься (не ERROR)
  • Проверим, сколько PASS vs FAIL

Шаг 3: Анализ Результатов

Сценарий А: Все тесты PASS ✅

Вывод: Логика PositionRoleBackend корректна

Действия:

  1. Проверить production конфигурацию:

    • AUTHENTICATION_BACKENDS order
    • Кэширование прав (Django cache)
    • LDAP синхронизация групп
  2. Проверить данные в БД:

    # Django shell
    user = User.objects.get(username='...')
    print(user.employee.position)
    print(user.employee.position.groups.all())
    print(user.get_all_permissions())  # Должны быть права из Position
    
  3. Проверить middleware и template context

Сценарий Б: Некоторые тесты FAIL ❌

Вывод: Есть проблема в логике PositionRoleBackend

Действия:

  1. Проанализировать failing assertions
  2. Исправить get_all_permissions() или has_perm()
  3. Убедиться что query Permission.objects.filter(group__positions=pos) корректен
  4. Проверить returns в формате "app_label.codename"

Проверка UI Visibility

Текущая Реализация

Файл: backend/templates/feed/feed_list.html (строка 19)

{% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
    <button class="btn btn-primary">Создать публикацию</button>
{% endif %}

Файл: backend/templates/documents/document_list.html (строки 17-21)

{% if user.is_staff or perms.documents.add_document %}
    <a href="{% url 'documents:document_create' %}" class="btn btn-primary">
        Добавить документ
    </a>
{% endif %}

Логика Проверки в Templates

Django использует perms.app_label.codename в шаблонах:

  1. Вызывает user.has_perm('app_label.codename')
  2. Проходит по всем AUTHENTICATION_BACKENDS
  3. Если хотя бы один backend вернёт True → permission granted

Требования:

  • PositionRoleBackend.has_perm() должен вернуть True
  • PositionRoleBackend.get_all_permissions() должен включать это право
  • Формат: tuple ('app_label', 'codename') или строка 'app_label.codename'

Отладочные Команды

Django Shell Testing

from django.contrib.auth.models import User, Permission, Group
from django.contrib.contenttypes.models import ContentType
from employees.models import Employee, Position

# Получить пользователя
user = User.objects.get(username='test_user')

# Проверить должность
print(f"Position: {user.employee.position}")
print(f"Position Groups: {user.employee.position.groups.all()}")

# Проверить права через backend
from eusrr_backend.auth_backends import PositionRoleBackend
backend = PositionRoleBackend()
all_perms = backend.get_all_permissions(user)
print(f"All Permissions: {all_perms}")

# Проверить has_perm
has_perm = backend.has_perm(user, 'feed.publish_company_post')
print(f"Has feed.publish_company_post: {has_perm}")

# Проверить через Django API
has_perm_django = user.has_perm('feed.publish_company_post')
print(f"User.has_perm('feed.publish_company_post'): {has_perm_django}")

SQL Query Debugging

from django.db import connection
from django.test.utils import CaptureQueriesContext

with CaptureQueriesContext(connection) as queries:
    perms = backend.get_all_permissions(user)
    
for q in queries:
    print(q['sql'])

LDAP Group Sync Check

from employees.ldap.services import PositionService

pos = Position.objects.get(name='Engineer')
dn = PositionService._ensure_position_group(pos)
print(f"LDAP Group DN: {dn}")

Ожидаемая SQL Query

При вызове get_all_permissions() должен выполниться query:

SELECT DISTINCT 
    django_content_type.app_label,
    auth_permission.codename
FROM auth_permission
INNER JOIN django_content_type 
    ON auth_permission.content_type_id = django_content_type.id
INNER JOIN auth_group_permissions 
    ON auth_permission.id = auth_group_permissions.permission_id
INNER JOIN auth_group 
    ON auth_group_permissions.group_id = auth_group.id
INNER JOIN employees_position_groups 
    ON auth_group.id = employees_position_groups.group_id
WHERE employees_position_groups.position_id = ?

Контрольный Чеклист

Pre-Test Checklist

  • Создан файл tests/employees/test_position_permissions.py
  • 13 тестов покрывают все сценарии
  • Фикстуры используют .filter().first() для Permission
  • Фикстуры доступны во всех test классах
  • Тесты запускаются без ERROR

Post-Test Checklist (если тесты проходят)

  • Проверить user.get_all_permissions() в production shell
  • Проверить user.has_perm('feed.publish_company_post') в production
  • Проверить что Position.groups содержит нужные группы
  • Проверить что Group.permissions содержит нужные права
  • Проверить порядок AUTHENTICATION_BACKENDS
  • Очистить Django permission cache
  • Перезапустить сервер

Post-Test Checklist (если тесты не проходят)

  • Проанализировать failing assertions
  • Исправить логику в PositionRoleBackend.get_all_permissions()
  • Исправить логику в PositionRoleBackend.has_perm()
  • Убедиться что query возвращает правильный формат
  • Проверить related_name в моделях
  • Перезапустить тесты

Следующие Шаги

  1. Сейчас: Исправить fixture visibility (перенести на module level)
  2. Потом: Запустить pytest с verbose output
  3. Далее: Проанализировать PASS/FAIL результаты
  4. Финал: Исправить production или backend в зависимости от результатов

Дополнительная Информация

Связанные Файлы

  • backend/eusrr_backend/auth_backends.py - PositionRoleBackend
  • backend/employees/models.py - Position, Employee
  • backend/templates/feed/feed_list.html - UI для публикаций
  • backend/templates/documents/document_list.html - UI для документов
  • backend/tests/employees/test_position_permissions.py - Тесты

Связанные Коммиты

  • 769f2ed - test: add comprehensive position permissions tests

Известные Ограничения

  • Права кэшируются Django (может требовать перезапуска)
  • LDAP синхронизация может задерживаться
  • Staff users всегда имеют все права (is_staff=True)

Документ создан: 2025-01-XX
Последнее обновление: ✅ ВСЕ ТЕСТЫ ПРОШЛИ (13/13 PASSED)
Статус: ✅ Готово к production тестированию

ВАЖНО: Что Делать Дальше

✅ Что Мы Выяснили

Тесты показали, что логика PositionRoleBackend работает ПРАВИЛЬНО!

  • ✅ Пользователи с должностями получают права из групп
  • ✅ Права обновляются при смене должности
  • ✅ Права удаляются при снятии должности
  • ✅ has_perm() и get_all_permissions() работают корректно
  • ✅ Добавлена проверка is_active (исправили баг)

🔍 Где Искать Проблему

Если кнопки всё ещё не появляются в production, проблема НЕ в коде, а в:

  1. Кэширование прав Django

    • Django кэширует права в _perm_cache и _user_perm_cache
    • После изменения должности нужен перелогин или очистка кэша
  2. LDAP синхронизация групп

    • Проверить что Position.groups содержит правильные группы
    • Проверить что Group.permissions содержит нужные права
  3. Порядок AUTHENTICATION_BACKENDS

    • PositionRoleBackend должен быть в списке (сейчас на 4 месте ✅)
    • Возможно другие backend'ы блокируют его работу

📋 Чеклист Отладки Production

Шаг 1: Проверить Права в Django Shell

from django.contrib.auth import get_user_model
User = get_user_model()

# Замените на реального пользователя
user = User.objects.get(username='имя_пользователя')

print(f"1. Position: {user.position}")
print(f"2. Position Groups: {user.position.groups.all() if user.position else 'NO POSITION'}")

if user.position:
    for group in user.position.groups.all():
        print(f"   - Group: {group.name}")
        print(f"     Permissions: {list(group.permissions.values_list('codename', flat=True))}")

print(f"\n3. User Permissions:")
print(f"   All Permissions: {user.get_all_permissions()}")
print(f"   Has feed.publish_company_post: {user.has_perm('feed.publish_company_post')}")
print(f"   Has documents.add_document: {user.has_perm('documents.add_document')}")

Ожидаемый результат:

  • Position должен быть не None
  • Position.groups должны содержать группы
  • Group.permissions должны включать нужные права
  • user.get_all_permissions() должен возвращать права из должности
  • user.has_perm('feed.publish_company_post') должен быть True

Шаг 2: Проверить Базу Данных

-- Проверить связь User → Employee → Position
SELECT u.username, e.id, p.name
FROM auth_user u
JOIN employees_employee e ON e.user_id = u.id
LEFT JOIN employees_position p ON e.position_id = p.id
WHERE u.username = 'имя_пользователя';

-- Проверить связь Position → Groups
SELECT p.name AS position, g.name AS group_name
FROM employees_position p
JOIN employees_position_groups pg ON pg.position_id = p.id
JOIN auth_group g ON pg.group_id = g.id
WHERE p.id = <position_id>;

-- Проверить связь Group → Permissions
SELECT g.name AS group_name, p.codename
FROM auth_group g
JOIN auth_group_permissions gp ON gp.group_id = g.id
JOIN auth_permission p ON gp.permission_id = p.id
WHERE g.id IN (
    SELECT group_id FROM employees_position_groups
    WHERE position_id = <position_id>
);

Шаг 3: Очистить Кэш Прав

from django.contrib.auth import get_user_model
User = get_user_model()

user = User.objects.get(username='имя_пользователя')

# Очистить кэши прав
if hasattr(user, '_perm_cache'):
    delattr(user, '_perm_cache')
if hasattr(user, '_user_perm_cache'):
    delattr(user, '_user_perm_cache')

# Или просто получить fresh instance
user = User.objects.get(id=user.id)

# Проверить снова
print(user.has_perm('feed.publish_company_post'))

Шаг 4: Проверить Template Context

В шаблоне feed/feed_list.html:

{# DEBUG: Покажите права пользователя #}
{% if user.is_authenticated %}
    <p>User: {{ user.username }}</p>
    <p>Position: {{ user.position }}</p>
    <p>Has publish_company_post: {{ perms.feed.publish_company_post }}</p>
    <p>Has publish_department_post: {{ perms.feed.publish_department_post }}</p>
    <p>Is Staff: {{ user.is_staff }}</p>
{% endif %}

Шаг 5: Проверить Middleware

В settings.py убедитесь что есть:

MIDDLEWARE = [
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
]

Шаг 6: Перезапустить Сервер

# Остановить
Ctrl+C

# Очистить кэш (если используется Redis/Memcached)
python manage.py shell
>>> from django.core.cache import cache
>>> cache.clear()
>>> exit()

# Запустить снова
python manage.py runserver

Шаг 7: Перелогинить Пользователя

  • Попросите пользователя выйти из системы
  • Войти заново
  • Проверить видимость кнопок

🐛 Возможные Проблемы

Проблема 1: Position.groups пустые

Симптом: user.position.groups.all() возвращает пустой список

Решение:

from employees.models import Position
from django.contrib.auth.models import Group

# Найти или создать группу
pub_group = Group.objects.get(name="publishers")

# Назначить группу должности
position = Position.objects.get(name="Инженер")
position.groups.add(pub_group)

Проблема 2: Group.permissions пустые

Симптом: group.permissions.all() возвращает пустой список

Решение:

from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType

# Найти Content Type
ct = ContentType.objects.get(app_label="feed", model="post")

# Найти или создать Permission
perm = Permission.objects.filter(
    codename="publish_company_post",
    content_type=ct
).first()

if not perm:
    perm = Permission.objects.create(
        codename="publish_company_post",
        name="Can publish company post",
        content_type=ct
    )

# Добавить в группу
pub_group = Group.objects.get(name="publishers")
pub_group.permissions.add(perm)

Проблема 3: LDAP не синхронизирует группы

Симптом: В LDAP нет группы POS_Инженер

Решение:

from employees.models import Position
from employees.ldap.services import PositionService

position = Position.objects.get(name="Инженер")
dn = PositionService._ensure_position_group(position)
print(f"LDAP Group DN: {dn}")

<EFBFBD> Итоговая Таблица Результатов Тестов

Тест Статус Что проверяет
test_user_without_position_has_no_permissions ✅ PASS Без должности = нет прав
test_user_with_position_gets_permissions ✅ PASS С должностью = права есть
test_position_change_updates_permissions ✅ PASS Смена должности обновляет права
test_remove_position_removes_permissions ✅ PASS Удаление должности убирает права
test_multiple_users_with_same_position ✅ PASS Несколько user одна должность
test_superuser_has_all_permissions ✅ PASS Superuser имеет все права
test_position_group_permissions_are_additive ✅ PASS Несколько групп складываются
test_get_all_permissions_includes_position_perms ✅ PASS get_all_permissions() работает
test_position_without_groups_grants_no_permissions ✅ PASS Должность без групп = нет прав
test_inactive_user_has_no_permissions ✅ PASS Неактивный user = нет прав
test_position_permissions_with_direct_user_permissions ✅ PASS Прямые + должность аддитивны
test_feed_permissions_through_position ✅ PASS Реальные feed права работают
test_documents_permissions_through_position ✅ PASS Реальные document права работают

🎯 Рекомендации

  1. Запустить отладочный скрипт (Шаг 1) чтобы увидеть текущее состояние
  2. Проверить LDAP синхронизацию группы для должности
  3. Очистить кэш и перелогинить пользователя
  4. Если проблема остаётся: Показать вывод отладочного скрипта

Результат должен быть: Кнопки появляются у пользователей с нужными должностями! 🎉


Документ создан: 2025-01-XX
Последнее обновление: ✅ ВСЕ ТЕСТЫ ПРОШЛИ (13/13 PASSED)
Статус: ✅ Готово к production тестированию