diff --git a/backend/documents/apps.py b/backend/documents/apps.py index d76e42f7..70360190 100644 --- a/backend/documents/apps.py +++ b/backend/documents/apps.py @@ -8,3 +8,4 @@ class DocumentsConfig(AppConfig): def ready(self): # импортируем сигналы, чтобы они подхватились import documents.notification_signals # noqa + import documents.rules # django-rules: регистрация предикатов и правил доступа diff --git a/backend/documents/rules.py b/backend/documents/rules.py new file mode 100644 index 00000000..eff25ee5 --- /dev/null +++ b/backend/documents/rules.py @@ -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 %} + + Редактировать + +{% endif %} + +{% has_rule 'documents.approve_document' user document as can_approve %} +{% if can_approve and document.status == 'pending' %} +
+ {% csrf_token %} + +
+{% 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) +""" diff --git a/backend/employees/apps.py b/backend/employees/apps.py index f5f88ccb..64346ff7 100644 --- a/backend/employees/apps.py +++ b/backend/employees/apps.py @@ -8,3 +8,4 @@ class EmployeesConfig(AppConfig): def ready(self): import employees.signals import employees.signals_birthday # Сигналы для синхронизации дней рождений с django-scheduler + import employees.rules # django-rules: регистрация предикатов и правил доступа diff --git a/backend/employees/rules.py b/backend/employees/rules.py new file mode 100644 index 00000000..44a3be83 --- /dev/null +++ b/backend/employees/rules.py @@ -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 %} + Редактировать +{% 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) +""" diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index e083f2f0..a5885196 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -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 бэкенд ] diff --git a/backend/requirements.txt b/backend/requirements.txt index 61d34542..5a6a286b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/docs/guides/DJANGO_RULES_INTEGRATION.md b/docs/guides/DJANGO_RULES_INTEGRATION.md new file mode 100644 index 00000000..d1d7a7be --- /dev/null +++ b/docs/guides/DJANGO_RULES_INTEGRATION.md @@ -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 %} + + Редактировать + +{% endif %} + +{% has_rule 'documents.approve_document' user document as can_approve %} +{% if can_approve and document.status == 'pending' %} +
+ {% csrf_token %} + +
+{% 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