feat: интеграция django-rules для object-level permissions

- Установлен пакет rules==3.5
- Добавлен ObjectPermissionBackend в AUTHENTICATION_BACKENDS
- Созданы базовые правила для employees:
  * Предикаты: is_hr_staff, is_department_head, is_own_profile и др.
  * Правила: view/change/delete employee, view reports и контакты
- Созданы базовые правила для documents:
  * Предикаты: is_document_owner, has_document_access, can_approve и др.
  * Правила: view/change/delete/approve/publish/share documents
- Правила регистрируются в apps.py (ready() метод)
- Создана подробная документация с примерами использования
  * В views, templates, DRF permissions, Django Admin
  * Сценарии: многоуровневое согласование, временные ограничения
  * Сравнение с django-guardian, отладка, тестирование

Документация: docs/guides/DJANGO_RULES_INTEGRATION.md
This commit is contained in:
igor_nadein 2026-02-28 00:07:53 +03:00
parent f1135dfe8a
commit 967819a58f
7 changed files with 1014 additions and 0 deletions

View File

@ -8,3 +8,4 @@ class DocumentsConfig(AppConfig):
def ready(self):
# импортируем сигналы, чтобы они подхватились
import documents.notification_signals # noqa
import documents.rules # django-rules: регистрация предикатов и правил доступа

296
backend/documents/rules.py Normal file
View File

@ -0,0 +1,296 @@
"""
django-rules: декларативные правила доступа для documents
Правила используются для проверки permissions на уровне объектов.
https://github.com/dfunckt/django-rules
"""
import rules
# -----------------------------------------------------------------------------
# ПРЕДИКАТЫ (predicates)
# -----------------------------------------------------------------------------
@rules.predicate
def is_superuser(user):
"""Суперпользователь имеет все права"""
return user.is_superuser
@rules.predicate
def is_document_owner(user, document):
"""Пользователь является создателем документа"""
if document is None:
return False
return document.created_by == user or document.owner == user
@rules.predicate
def is_document_author(user, document):
"""Пользователь является автором документа"""
if document is None or not hasattr(document, 'author'):
return False
return document.author == user
@rules.predicate
def has_document_access(user, document):
"""
Пользователь имеет доступ к документу через:
- Публичный доступ (is_public)
- Доступ для отдела (department_access)
- Прямой доступ в списке разрешённых пользователей
"""
if document is None:
return False
# Публичный документ
if hasattr(document, 'is_public') and document.is_public:
return True
# Доступ для отдела
if hasattr(document, 'department_access') and hasattr(user, 'department'):
if document.department_access and document.department_access == user.department:
return True
# Прямой доступ через ManyToMany (если есть поле shared_with или allowed_users)
if hasattr(document, 'shared_with'):
if user in document.shared_with.all():
return True
if hasattr(document, 'allowed_users'):
if user in document.allowed_users.all():
return True
return False
@rules.predicate
def can_approve_documents(user):
"""
Пользователь может согласовывать документы.
Адаптируйте под вашу логику (по должности, группе и т.д.)
"""
if not hasattr(user, 'position'):
return False
position_name = getattr(user.position, 'name', '').lower()
return any(keyword in position_name for keyword in [
'руководитель', 'начальник', 'директор', 'заведующий'
])
@rules.predicate
def is_document_approver(user, document):
"""Пользователь назначен согласующим для этого документа"""
if document is None:
return False
# Проверка через связь approvers, если есть
if hasattr(document, 'approvers'):
return user in document.approvers.all()
# Проверка через связь approval_chain (если используется цепочка согласования)
if hasattr(document, 'approval_chain'):
return document.approval_chain.filter(approver=user).exists()
return False
@rules.predicate
def is_same_department(user, document):
"""Документ относится к отделу пользователя"""
if document is None or not hasattr(user, 'department'):
return False
if hasattr(document, 'department'):
return document.department == user.department
return False
@rules.predicate
def is_document_category_manager(user, document):
"""
Пользователь является менеджером категории документа.
Адаптируйте под вашу структуру категорий.
"""
if document is None or not hasattr(document, 'category'):
return False
category = document.category
if hasattr(category, 'managers'):
return user in category.managers.all()
return False
# -----------------------------------------------------------------------------
# ПРАВИЛА (rules)
# -----------------------------------------------------------------------------
# Просмотр документа
rules.add_rule(
'documents.view_document',
is_superuser | is_document_owner | is_document_author | has_document_access
)
# Изменение документа (только владелец и автор)
rules.add_rule(
'documents.change_document',
is_superuser | is_document_owner | is_document_author
)
# Удаление документа (только владелец и superuser)
rules.add_rule(
'documents.delete_document',
is_superuser | is_document_owner
)
# Согласование документа (согласующие + руководители)
rules.add_rule(
'documents.approve_document',
is_superuser | is_document_approver | can_approve_documents
)
# Публикация документа (владелец + менеджеры категории)
rules.add_rule(
'documents.publish_document',
is_superuser | is_document_owner | is_document_category_manager
)
# Скачивание документа (все, кто имеет доступ на просмотр)
rules.add_rule(
'documents.download_document',
is_superuser | is_document_owner | is_document_author | has_document_access
)
# Выдача доступа другим пользователям (только владелец)
rules.add_rule(
'documents.share_document',
is_superuser | is_document_owner
)
# Просмотр истории изменений документа
rules.add_rule(
'documents.view_document_history',
is_superuser | is_document_owner | is_document_author | is_document_category_manager
)
# Просмотр всех документов отдела
rules.add_rule(
'documents.view_department_documents',
is_superuser | can_approve_documents
)
# -----------------------------------------------------------------------------
# PERMISSIONS ДЛЯ МОДЕЛЕЙ
# -----------------------------------------------------------------------------
# Если вы хотите переопределить стандартные Django permissions:
# rules.add_perm('documents.view_document', is_superuser | has_document_access)
# rules.add_perm('documents.change_document', is_superuser | is_document_owner)
# rules.add_perm('documents.delete_document', is_superuser | is_document_owner)
# rules.add_perm('documents.add_document', rules.is_authenticated) # Все авторизованные могут создавать
# -----------------------------------------------------------------------------
# ИСПОЛЬЗОВАНИЕ В КОДЕ
# -----------------------------------------------------------------------------
"""
# В views:
from django.core.exceptions import PermissionDenied
import rules
def document_detail(request, pk):
document = get_object_or_404(Document, pk=pk)
# Проверка доступа
if not rules.test_rule('documents.view_document', request.user, document):
raise PermissionDenied
return render(request, 'documents/detail.html', {'document': document})
def document_approve(request, pk):
document = get_object_or_404(Document, pk=pk)
# Проверка прав на согласование
if not rules.test_rule('documents.approve_document', request.user, document):
raise PermissionDenied
# Логика согласования
document.status = 'approved'
document.approved_by = request.user
document.save()
return redirect('documents:detail', pk=document.pk)
# В templates:
{% load rules %}
{% has_rule 'documents.change_document' user document as can_edit %}
{% if can_edit %}
<a href="{% url 'documents:edit' document.pk %}" class="btn btn-primary">
Редактировать
</a>
{% endif %}
{% has_rule 'documents.approve_document' user document as can_approve %}
{% if can_approve and document.status == 'pending' %}
<form method="post" action="{% url 'documents:approve' document.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-success">Согласовать</button>
</form>
{% endif %}
# В DRF permissions:
from rest_framework import permissions
import rules
class DocumentPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('documents.view_document', request.user, obj)
elif request.method in ['PUT', 'PATCH']:
return rules.test_rule('documents.change_document', request.user, obj)
elif request.method == 'DELETE':
return rules.test_rule('documents.delete_document', request.user, obj)
return False
# В queryset filtering (показывать только доступные документы):
from django.db.models import Q
def get_accessible_documents(user):
# Документы, к которым есть доступ
return Document.objects.filter(
Q(created_by=user) | # Созданные пользователем
Q(owner=user) | # Принадлежащие пользователю
Q(is_public=True) | # Публичные
Q(department_access=user.department) | # Доступные для отдела
Q(shared_with=user) # Расшаренные для пользователя
).distinct()
# Middleware для проверки доступа к файлам:
# (если файлы отдаются через X-Accel-Redirect или подобное)
class DocumentAccessMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith('/media/documents/'):
# Извлекаем документ по пути
document = self.get_document_from_path(request.path)
if document and not rules.test_rule('documents.view_document', request.user, document):
return HttpResponseForbidden("У вас нет доступа к этому документу")
return self.get_response(request)
"""

View File

@ -8,3 +8,4 @@ class EmployeesConfig(AppConfig):
def ready(self):
import employees.signals
import employees.signals_birthday # Сигналы для синхронизации дней рождений с django-scheduler
import employees.rules # django-rules: регистрация предикатов и правил доступа

186
backend/employees/rules.py Normal file
View File

@ -0,0 +1,186 @@
"""
django-rules: декларативные правила доступа для employees
Правила используются для проверки permissions на уровне объектов.
https://github.com/dfunckt/django-rules
"""
import rules
# -----------------------------------------------------------------------------
# ПРЕДИКАТЫ (predicates)
# -----------------------------------------------------------------------------
@rules.predicate
def is_superuser(user):
"""Суперпользователь имеет все права"""
return user.is_superuser
@rules.predicate
def is_staff(user):
"""Сотрудник (is_staff) имеет базовые права"""
return user.is_staff
@rules.predicate
def is_hr_staff(user):
"""
HR сотрудник - определяется по должности или группе.
Адаптируйте под вашу логику: по position, department, группе и т.д.
"""
if not hasattr(user, 'position'):
return False
position_name = getattr(user.position, 'name', '').lower()
return 'hr' in position_name or 'кадр' in position_name
@rules.predicate
def is_department_head(user):
"""
Руководитель отдела - определяется по должности.
Адаптируйте под вашу логику.
"""
if not hasattr(user, 'position'):
return False
position_name = getattr(user.position, 'name', '').lower()
return any(keyword in position_name for keyword in [
'руководитель', 'начальник', 'директор', 'заведующий', 'глава'
])
@rules.predicate
def is_own_profile(user, employee):
"""Пользователь просматривает свой собственный профиль"""
if employee is None:
return False
return user.pk == employee.pk
@rules.predicate
def is_same_department(user, employee):
"""Пользователь и сотрудник в одном отделе"""
if employee is None or not hasattr(user, 'department') or not hasattr(employee, 'department'):
return False
return user.department == employee.department
# -----------------------------------------------------------------------------
# ПРАВИЛА (rules)
# -----------------------------------------------------------------------------
# Просмотр профиля сотрудника
rules.add_rule(
'employees.view_employee',
is_superuser | is_staff | is_hr_staff | is_own_profile
)
# Изменение профиля сотрудника
rules.add_rule(
'employees.change_employee',
is_superuser | is_hr_staff | (is_own_profile & ~rules.always_deny)
)
# Удаление сотрудника (только HR и superuser)
rules.add_rule(
'employees.delete_employee',
is_superuser | is_hr_staff
)
# Просмотр списка всех сотрудников
rules.add_rule(
'employees.view_all_employees',
is_superuser | is_staff | is_hr_staff
)
# Просмотр отчётов по персоналу (только HR и руководители)
rules.add_rule(
'employees.view_reports',
is_superuser | is_hr_staff | is_department_head
)
# Изменение должности сотрудника (только HR)
rules.add_rule(
'employees.change_position',
is_superuser | is_hr_staff
)
# Изменение отдела сотрудника (только HR)
rules.add_rule(
'employees.change_department',
is_superuser | is_hr_staff
)
# Просмотр контактной информации сотрудника
rules.add_rule(
'employees.view_contact_info',
is_superuser | is_staff | is_own_profile | is_same_department
)
# -----------------------------------------------------------------------------
# PERMISSIONS ДЛЯ МОДЕЛЕЙ
# -----------------------------------------------------------------------------
# Если вы хотите переопределить стандартные Django permissions для модели:
# rules.add_perm('employees.view_employee', is_superuser | is_staff)
# rules.add_perm('employees.change_employee', is_superuser | is_hr_staff)
# rules.add_perm('employees.delete_employee', is_superuser | is_hr_staff)
# rules.add_perm('employees.add_employee', is_superuser | is_hr_staff)
# -----------------------------------------------------------------------------
# ИСПОЛЬЗОВАНИЕ В КОДЕ
# -----------------------------------------------------------------------------
"""
# В views:
from django.core.exceptions import PermissionDenied
def employee_detail(request, pk):
employee = get_object_or_404(Employee, pk=pk)
# Проверка через has_perm (если правило зарегистрировано через add_perm)
if not request.user.has_perm('employees.view_employee', employee):
raise PermissionDenied
# Или напрямую через rules.test_rule
if not rules.test_rule('employees.view_employee', request.user, employee):
raise PermissionDenied
return render(request, 'employees/detail.html', {'employee': employee})
# В templates:
{% load rules %}
{% has_perm 'employees.change_employee' user employee as can_change %}
{% if can_change %}
<a href="{% url 'employees:edit' employee.pk %}">Редактировать</a>
{% endif %}
# В DRF permissions:
from rest_framework import permissions
import rules
class EmployeePermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('employees.view_employee', request.user, obj)
elif request.method in ['PUT', 'PATCH']:
return rules.test_rule('employees.change_employee', request.user, obj)
elif request.method == 'DELETE':
return rules.test_rule('employees.delete_employee', request.user, obj)
return False
# В admin.py для object-level permissions:
from django.contrib import admin
class EmployeeAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if obj is None:
return super().has_change_permission(request, obj)
return rules.test_rule('employees.change_employee', request.user, obj)
"""

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
"widget_tweaks",
"simple_history",
"rest_framework",
"rules", # django-rules для декларативных permissions
# Celery приложения
"django_celery_beat", # Периодические задачи
"django_celery_results", # Хранение результатов
@ -347,6 +348,7 @@ AUTHENTICATION_BACKENDS = [
"eusrr_backend.auth_backends.SuperuserOnlyBackend", # экстренный доступ для админа
# расчёт прав на основе должностей
"eusrr_backend.auth_backends.PositionRoleBackend",
"rules.permissions.ObjectPermissionBackend", # django-rules для object-level permissions
"django.contrib.auth.backends.ModelBackend", # стандартный Django бэкенд
]

View File

@ -102,6 +102,7 @@ qrcode==8.1
redis==6.4.0
requests==2.32.4
requests-oauthlib==2.0.0
rules==3.5
service-identity==24.2.0
six==1.17.0
social-auth-app-django==5.5.1

View File

@ -0,0 +1,527 @@
# Интеграция django-rules: Object-Level Permissions
**Дата:** 28 февраля 2026
**Статус:** ✅ Базовая интеграция завершена
**Ветка:** `feature/django-rules`
## Обзор
django-rules — это библиотека для декларативного управления правами доступа на уровне объектов в Django. Она позволяет определять сложную логику permissions через композицию простых предикатов.
### Преимущества
- ✅ **Object-level permissions**: проверка прав на конкретные объекты
- ✅ **Декларативный синтаксис**: правила определяются явно и читаемо
- ✅ **Композиция**: комбинирование предикатов через `&`, `|`, `~`
- ✅ **Интеграция**: работает с Django, DRF, Admin, Templates
- ✅ **Производительность**: легковесная библиотека без overhead
- ✅ **Тестируемость**: правила легко покрываются unit-тестами
### Ссылки
- **GitHub**: https://github.com/dfunckt/django-rules
- **Документация**: https://github.com/dfunckt/django-rules#rules
- **PyPI**: https://pypi.org/project/rules/
---
## Установка и настройка
### 1. Установка пакета
```bash
../.venv/Scripts/pip install rules==3.5
```
Добавлено в `requirements.txt`:
```
rules==3.5
```
### 2. Настройка Django
**settings.py**:
```python
INSTALLED_APPS = [
# ...
"rules", # django-rules для декларативных permissions
# ...
]
AUTHENTICATION_BACKENDS = [
"eusrr_backend.auth_backends.LDAP3Backend",
"eusrr_backend.auth_backends.EmailOrPhoneBackend",
"eusrr_backend.auth_backends.SuperuserOnlyBackend",
"eusrr_backend.auth_backends.PositionRoleBackend",
"rules.permissions.ObjectPermissionBackend", # ← django-rules backend
"django.contrib.auth.backends.ModelBackend",
]
```
### 3. Создание файлов правил
Созданы базовые правила для двух приложений:
- **employees/rules.py**: правила доступа к профилям сотрудников
- **documents/rules.py**: правила доступа к документам
---
## Архитектура правил
### Базовая структура
```python
import rules
# 1. ПРЕДИКАТЫ - базовые проверки (возвращают True/False)
@rules.predicate
def is_document_owner(user, document):
return document.owner == user
# 2. ПРАВИЛА - композиция предикатов
rules.add_rule(
'documents.view_document',
is_superuser | is_document_owner | has_document_access
)
# 3. PERMISSIONS (опционально) - регистрация в Django permissions
rules.add_perm('documents.view_document', is_superuser | is_document_owner)
```
### Композиция предикатов
```python
# Логическое И (AND)
rule = predicate1 & predicate2
# Логическое ИЛИ (OR)
rule = predicate1 | predicate2
# Логическое НЕ (NOT)
rule = ~predicate1
# Сложные комбинации
rule = (is_owner | is_admin) & ~is_banned & has_subscription
```
---
## Реализованные правила
### Employees (employees/rules.py)
#### Предикаты:
- `is_superuser` — пользователь является суперюзером
- `is_staff` — пользователь имеет статус персонала
- `is_hr_staff` — HR сотрудник (по должности)
- `is_department_head` — руководитель отдела
- `is_own_profile` — собственный профиль
- `is_same_department` — сотрудники в одном отделе
#### Правила:
- `employees.view_employee` — просмотр профиля
- `employees.change_employee` — изменение профиля
- `employees.delete_employee` — удаление сотрудника
- `employees.view_all_employees` — просмотр списка
- `employees.view_reports` — просмотр отчётов
- `employees.change_position` — изменение должности
- `employees.change_department` — изменение отдела
- `employees.view_contact_info` — просмотр контактов
### Documents (documents/rules.py)
#### Предикаты:
- `is_document_owner` — владелец документа
- `is_document_author` — автор документа
- `has_document_access` — доступ через is_public/department/shared
- `can_approve_documents` — может согласовывать (по должности)
- `is_document_approver` — назначен согласующим
- `is_same_department` — документ отдела пользователя
- `is_document_category_manager` — менеджер категории
#### Правила:
- `documents.view_document` — просмотр документа
- `documents.change_document` — изменение документа
- `documents.delete_document` — удаление документа
- `documents.approve_document` — согласование документа
- `documents.publish_document` — публикация документа
- `documents.download_document` — скачивание документа
- `documents.share_document` — выдача доступа
- `documents.view_document_history` — просмотр истории
- `documents.view_department_documents` — документы отдела
---
## Использование
### 1. В Views
```python
from django.core.exceptions import PermissionDenied
import rules
def document_detail(request, pk):
document = get_object_or_404(Document, pk=pk)
# Проверка через rules.test_rule
if not rules.test_rule('documents.view_document', request.user, document):
raise PermissionDenied
return render(request, 'documents/detail.html', {'document': document})
def document_approve(request, pk):
document = get_object_or_404(Document, pk=pk)
if not rules.test_rule('documents.approve_document', request.user, document):
raise PermissionDenied
document.status = 'approved'
document.approved_by = request.user
document.save()
return redirect('documents:detail', pk=document.pk)
```
### 2. В Templates
```django
{% load rules %}
{% has_rule 'documents.change_document' user document as can_edit %}
{% if can_edit %}
<a href="{% url 'documents:edit' document.pk %}" class="btn btn-primary">
Редактировать
</a>
{% endif %}
{% has_rule 'documents.approve_document' user document as can_approve %}
{% if can_approve and document.status == 'pending' %}
<form method="post" action="{% url 'documents:approve' document.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-success">Согласовать</button>
</form>
{% endif %}
```
### 3. В DRF Permissions
```python
from rest_framework import permissions
import rules
class DocumentPermission(permissions.BasePermission):
"""Object-level permission для Document через django-rules"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('documents.view_document', request.user, obj)
elif request.method in ['PUT', 'PATCH']:
return rules.test_rule('documents.change_document', request.user, obj)
elif request.method == 'DELETE':
return rules.test_rule('documents.delete_document', request.user, obj)
return False
# В ViewSet:
class DocumentViewSet(viewsets.ModelViewSet):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
permission_classes = [permissions.IsAuthenticated, DocumentPermission]
```
### 4. В Django Admin
```python
from django.contrib import admin
import rules
class DocumentAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if obj is None:
# Проверка на уровне модели (list view)
return super().has_change_permission(request, obj)
# Проверка на уровне объекта (change view)
return rules.test_rule('documents.change_document', request.user, obj)
def has_delete_permission(self, request, obj=None):
if obj is None:
return super().has_delete_permission(request, obj)
return rules.test_rule('documents.delete_document', request.user, obj)
```
### 5. Фильтрация QuerySet
```python
from django.db.models import Q
def get_accessible_documents(user):
"""Возвращает документы, к которым пользователь имеет доступ"""
return Document.objects.filter(
Q(created_by=user) | # Созданные пользователем
Q(owner=user) | # Принадлежащие пользователю
Q(is_public=True) | # Публичные
Q(department_access=user.department) | # Для отдела
Q(shared_with=user) # Расшаренные
).distinct()
```
### 6. Model-level Permissions (опционально)
Если нужно интегрировать с Django permissions (`user.has_perm()`):
```python
# В rules.py:
rules.add_perm('documents.view_document', is_superuser | has_document_access)
rules.add_perm('documents.change_document', is_superuser | is_document_owner)
# Теперь можно использовать стандартный API:
if request.user.has_perm('documents.view_document', document):
# ...
```
---
## Тестирование
### Unit-тесты для предикатов
```python
import pytest
import rules
from employees.rules import is_own_profile, is_hr_staff
@pytest.mark.django_db
def test_is_own_profile():
user = Employee.objects.create(username='test')
other = Employee.objects.create(username='other')
assert is_own_profile(user, user) is True
assert is_own_profile(user, other) is False
@pytest.mark.django_db
def test_is_hr_staff():
hr_position = Position.objects.create(name='HR Manager')
dev_position = Position.objects.create(name='Developer')
hr_user = Employee.objects.create(username='hr', position=hr_position)
dev_user = Employee.objects.create(username='dev', position=dev_position)
assert is_hr_staff(hr_user) is True
assert is_hr_staff(dev_user) is False
```
### Интеграционные тесты для правил
```python
import pytest
import rules
@pytest.mark.django_db
def test_view_employee_permission():
hr = Employee.objects.create(username='hr', position=hr_position)
employee = Employee.objects.create(username='employee')
# HR может просматривать любого сотрудника
assert rules.test_rule('employees.view_employee', hr, employee)
# Сотрудник может просматривать свой профиль
assert rules.test_rule('employees.view_employee', employee, employee)
```
---
## Следующие шаги
### 1. Адаптация под реальные модели
Текущие правила содержат примеры и комментарии. Необходимо:
- ✅ Проверить наличие полей в моделях (`owner`, `is_public`, `shared_with` и т.д.)
- ✅ Адаптировать предикаты под реальную структуру
- ✅ Убрать неиспользуемые предикаты
- ✅ Добавить недостающие правила
### 2. Миграция существующих permissions
Заменить ручные проверки на правила:
```python
# Старый код:
if not (request.user.is_superuser or document.owner == request.user):
raise PermissionDenied
# Новый код:
if not rules.test_rule('documents.change_document', request.user, document):
raise PermissionDenied
```
### 3. Покрытие тестами
Создать тесты для всех критичных правил:
- `employees/tests/test_rules.py`
- `documents/tests/test_rules.py`
### 4. Расширение на другие приложения
Создать `rules.py` для:
- `calendar_app` — доступ к календарям и событиям
- `requests_app` — создание и обработка заявок
- `communications` — доступ к чатам и сообщениям
- `finance` — финансовые документы и отчёты
### 5. Performance optimization
Для больших выборок использовать фильтрацию на уровне БД:
```python
# Вместо:
documents = [d for d in Document.objects.all() if rules.test_rule('view', user, d)]
# Использовать:
documents = get_accessible_documents(user)
```
### 6. Документация для команды
- Создать примеры использования для каждого приложения
- Обновить CODE_STYLE.md с правилами написания predicates
- Провести код-ревью существующих permissions
---
## Сравнение с django-guardian
| Аспект | django-rules | django-guardian |
|--------|--------------|-----------------|
| **Хранение** | В коде (декларативно) | В БД (таблицы permissions) |
| **Производительность** | Быстрее (нет запросов в БД) | Медленнее (JOIN с таблицами) |
| **Гибкость** | Высокая (любая логика) | Ограничена (user-object pairs) |
| **Миграции** | Не требуются | Нужна БД миграция |
| **Сложность** | Проще | Сложнее |
| **Масштабирование** | Отлично для ~1K объектов | Лучше для миллионов объектов |
**Вывод**: для EUSRR с умеренным количеством объектов и сложной логикой прав django-rules — оптимальный выбор.
---
## Примеры реальных сценариев
### Сценарий 1: Многоуровневое согласование
```python
@rules.predicate
def can_approve_at_current_stage(user, document):
"""Проверка, что пользователь — текущий согласующий"""
current_stage = document.approval_chain.filter(status='pending').first()
if not current_stage:
return False
return current_stage.approver == user
rules.add_rule(
'documents.approve_at_stage',
is_superuser | can_approve_at_current_stage
)
```
### Сценарий 2: Временные ограничения
```python
from django.utils import timezone
from datetime import timedelta
@rules.predicate
def can_edit_recent_document(user, document):
"""Редактирование только в течение 24 часов после создания"""
if not is_document_owner(user, document):
return False
return timezone.now() - document.created_at < timedelta(hours=24)
rules.add_rule(
'documents.quick_edit',
can_edit_recent_document
)
```
### Сценарий 3: Иерархия отделов
```python
@rules.predicate
def is_parent_department_head(user, employee):
"""Руководитель вышестоящего отдела"""
if not hasattr(employee, 'department') or not hasattr(employee.department, 'parent'):
return False
return (
hasattr(user, 'department') and
user.department == employee.department.parent and
is_department_head(user)
)
rules.add_rule(
'employees.view_subordinate',
is_superuser | is_department_head | is_parent_department_head
)
```
---
## Отладка и мониторинг
### Логирование проверок прав
```python
import logging
logger = logging.getLogger(__name__)
@rules.predicate
def is_document_owner(user, document):
result = document.owner == user
logger.debug(
f"Permission check: is_document_owner({user.username}, doc_{document.pk}) = {result}"
)
return result
```
### Отладочная view для проверки прав
```python
def debug_permissions(request, document_id):
"""DEBUG view для проверки прав доступа"""
if not settings.DEBUG:
raise Http404
document = get_object_or_404(Document, pk=document_id)
checks = {
'view': rules.test_rule('documents.view_document', request.user, document),
'change': rules.test_rule('documents.change_document', request.user, document),
'delete': rules.test_rule('documents.delete_document', request.user, document),
'approve': rules.test_rule('documents.approve_document', request.user, document),
}
return JsonResponse(checks)
```
---
## Заключение
Базовая интеграция django-rules завершена:
- ✅ Пакет установлен и настроен
- ✅ Созданы примеры правил для employees и documents
- ✅ Документация и примеры использования готовы
Следующий шаг — адаптация правил под реальную структуру моделей и миграция существующих проверок прав на использование django-rules.
---
**Автор**: GitHub Copilot
**Дата**: 28 февраля 2026