Corp/backend/POSITION_PERMISSIONS_TESTING.md
Игорь Надеин 57b2d856a1 docs: document final solution for position permissions issue
Root cause identified:
- Template checked non-existent permissions (publish_company_post, publish_department_post)
- Django only creates 4 default permissions: add, change, delete, view
- User actually HAD feed.add_post permission from Position→Group chain

Solution:
- Changed template check to use feed.add_post (real permission)
- Confirmed PositionRoleBackend works correctly (13/13 tests passed)
- User can now see 'Create Post' button

Production diagnostic confirmed user has permissions through position:
Position 'СММ специалист' → Group 'Дизайн СММ Реклама' → Permissions include feed.add_post
2025-12-08 14:17:06 +03:00

762 lines
29 KiB
Markdown
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.

# Тестирование Прав Доступа Через Должности (Position Permissions)
## ✅ РЕШЕНИЕ НАЙДЕНО И ПРИМЕНЕНО
**Дата:** 8 декабря 2025
**Статус:** ✅ Исправлено и протестировано
### Корневая Причина
В шаблоне `feed/feed_list.html` проверялись **несуществующие** права:
```django
{% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
```
Эти права **никогда не создавались** в Django! Правильное право для создания постов:
```django
{% if user.is_staff or perms.feed.add_post %}
```
### Диагностика Production
Запущен отладочный скрипт для пользователя `maze3290@gmail.com`:
```python
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)
```diff
- {% 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`
```python
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)
```python
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`
```python
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)
```python
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)
```python
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)
```python
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()` в фикстурах приводит к попыткам создать дубликаты.
**Решение:**
Изменить паттерн создания:
```python
# ❌ Старый код
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
- [x] Заменить `get_or_create` на `filter().first()` + conditional create
- [x] Применить в `group_with_publish_perm`
- [x] Применить в `group_with_document_perm`
- [x] Применить в `test_position_group_permissions_are_additive`
- [ ] Перенести фикстуры на module level
### Шаг 2: Запуск Тестов
```bash
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. Проверить данные в БД:
```python
# 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)
```html
{% 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)
```html
{% 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
```python
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
```python
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
```python
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:
```sql
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
- [x] Создан файл `tests/employees/test_position_permissions.py`
- [x] 13 тестов покрывают все сценарии
- [x] Фикстуры используют `.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
```python
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: Проверить Базу Данных
```sql
-- Проверить связь 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: Очистить Кэш Прав
```python
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`:
```django
{# 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` убедитесь что есть:
```python
MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
```
#### Шаг 6: Перезапустить Сервер
```bash
# Остановить
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()` возвращает пустой список
**Решение:**
```python
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()` возвращает пустой список
**Решение:**
```python
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_Инженер`
**Решение:**
```python
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}")
```
### <20> Итоговая Таблица Результатов Тестов
| Тест | Статус | Что проверяет |
|------|--------|---------------|
| 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 тестированию