igor_nadein d3b648b438 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
2026-02-28 00:07:53 +03:00

297 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)
"""