Merge branch 'feature/django-rules' into develop
This commit is contained in:
commit
01f0cce2c6
@ -19,6 +19,7 @@ class CalendarAppConfig(AppConfig):
|
||||
"""
|
||||
import calendar_app.notification_signals # noqa: F401
|
||||
from calendar_app import signals # noqa: F401 - старая система
|
||||
import calendar_app.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
try:
|
||||
Employee = django_apps.get_model("employees", "Employee")
|
||||
|
||||
376
backend/calendar_app/rules.py
Normal file
376
backend/calendar_app/rules.py
Normal file
@ -0,0 +1,376 @@
|
||||
"""
|
||||
django-rules: декларативные правила доступа для calendar_app
|
||||
|
||||
Правила используются для проверки permissions на уровне объектов.
|
||||
https://github.com/dfunckt/django-rules
|
||||
"""
|
||||
|
||||
import rules
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРЕДИКАТЫ (predicates)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@rules.predicate
|
||||
def is_superuser(user):
|
||||
"""Суперпользователь имеет все права"""
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_calendar_owner(user, calendar):
|
||||
"""Пользователь является владельцем календаря"""
|
||||
if calendar is None:
|
||||
return False
|
||||
|
||||
# Для django-scheduler
|
||||
if hasattr(calendar, 'creator'):
|
||||
return calendar.creator == user
|
||||
|
||||
# Альтернативные поля
|
||||
if hasattr(calendar, 'owner'):
|
||||
return calendar.owner == user
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_personal_calendar(user, calendar):
|
||||
"""Это личный календарь пользователя"""
|
||||
if calendar is None:
|
||||
return False
|
||||
|
||||
# Проверка slug для личного календаря
|
||||
if hasattr(calendar, 'slug'):
|
||||
return calendar.slug == f'personal-{user.pk}'
|
||||
|
||||
# Проверка через creator + тип календаря
|
||||
if hasattr(calendar, 'calendar_type') and calendar.calendar_type == 'personal':
|
||||
return is_calendar_owner(user, calendar)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def has_calendar_access(user, calendar):
|
||||
"""Пользователь имеет доступ к календарю через подписку или права"""
|
||||
if calendar is None:
|
||||
return False
|
||||
|
||||
# Публичный календарь
|
||||
if hasattr(calendar, 'is_public') and calendar.is_public:
|
||||
return True
|
||||
|
||||
# Календарь отдела
|
||||
if hasattr(calendar, 'department') and hasattr(user, 'department'):
|
||||
if calendar.department == user.department:
|
||||
return True
|
||||
|
||||
# Подписка на календарь
|
||||
if hasattr(calendar, 'subscriptions'):
|
||||
return calendar.subscriptions.filter(user=user).exists()
|
||||
|
||||
# Календарь компании (доступен всем сотрудникам)
|
||||
if hasattr(calendar, 'calendar_type') and calendar.calendar_type == 'company':
|
||||
return user.is_staff
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_event_creator(user, event):
|
||||
"""Пользователь является создателем события"""
|
||||
if event is None:
|
||||
return False
|
||||
|
||||
# django-scheduler использует creator_id
|
||||
if hasattr(event, 'creator'):
|
||||
return event.creator == user
|
||||
|
||||
if hasattr(event, 'created_by'):
|
||||
return event.created_by == user
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_access_event_calendar(user, event):
|
||||
"""Пользователь имеет доступ к календарю, где находится событие"""
|
||||
if event is None or not hasattr(event, 'calendar'):
|
||||
return False
|
||||
|
||||
return has_calendar_access(user, event.calendar) or is_calendar_owner(user, event.calendar)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_event_participant(user, event):
|
||||
"""Пользователь является участником события"""
|
||||
if event is None:
|
||||
return False
|
||||
|
||||
# Проверка через участников
|
||||
if hasattr(event, 'participants'):
|
||||
return user in event.participants.all()
|
||||
|
||||
# Проверка через occurrence (для повторяющихся событий)
|
||||
if hasattr(event, 'occurrence_set'):
|
||||
return event.occurrence_set.filter(participants=user).exists()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_manage_calendars(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_department_calendar(user, calendar):
|
||||
"""Календарь относится к отделу пользователя"""
|
||||
if calendar is None or not hasattr(user, 'department'):
|
||||
return False
|
||||
|
||||
if hasattr(calendar, 'department'):
|
||||
return calendar.department == user.department
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРАВИЛА (rules)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Просмотр календаря
|
||||
rules.add_rule(
|
||||
'calendar_app.view_calendar',
|
||||
is_superuser | is_calendar_owner | is_personal_calendar |
|
||||
has_calendar_access | is_department_calendar
|
||||
)
|
||||
|
||||
# Изменение календаря (настройки, название, описание)
|
||||
rules.add_rule(
|
||||
'calendar_app.change_calendar',
|
||||
is_superuser | is_calendar_owner | can_manage_calendars
|
||||
)
|
||||
|
||||
# Удаление календаря
|
||||
rules.add_rule(
|
||||
'calendar_app.delete_calendar',
|
||||
is_superuser | is_calendar_owner
|
||||
)
|
||||
|
||||
# Создание события в календаре
|
||||
rules.add_rule(
|
||||
'calendar_app.create_event',
|
||||
is_superuser | is_calendar_owner | has_calendar_access
|
||||
)
|
||||
|
||||
# Просмотр события
|
||||
rules.add_rule(
|
||||
'calendar_app.view_event',
|
||||
is_superuser | is_event_creator | can_access_event_calendar | is_event_participant
|
||||
)
|
||||
|
||||
# Изменение события
|
||||
rules.add_rule(
|
||||
'calendar_app.change_event',
|
||||
is_superuser | is_event_creator | is_calendar_owner
|
||||
)
|
||||
|
||||
# Удаление события
|
||||
rules.add_rule(
|
||||
'calendar_app.delete_event',
|
||||
is_superuser | is_event_creator | is_calendar_owner
|
||||
)
|
||||
|
||||
# Подписка на календарь
|
||||
rules.add_rule(
|
||||
'calendar_app.subscribe_calendar',
|
||||
rules.is_authenticated # Любой авторизованный может подписаться на публичный календарь
|
||||
)
|
||||
|
||||
# Отмена подписки на календарь
|
||||
rules.add_rule(
|
||||
'calendar_app.unsubscribe_calendar',
|
||||
rules.is_authenticated
|
||||
)
|
||||
|
||||
# Экспорт календаря (iCal)
|
||||
rules.add_rule(
|
||||
'calendar_app.export_calendar',
|
||||
is_superuser | is_calendar_owner | has_calendar_access
|
||||
)
|
||||
|
||||
# Приглашение участников на событие
|
||||
rules.add_rule(
|
||||
'calendar_app.invite_participants',
|
||||
is_superuser | is_event_creator | is_calendar_owner
|
||||
)
|
||||
|
||||
# Просмотр всех календарей (для администраторов)
|
||||
rules.add_rule(
|
||||
'calendar_app.view_all_calendars',
|
||||
is_superuser | can_manage_calendars
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ИСПОЛЬЗОВАНИЕ В КОДЕ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
# В views:
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import rules
|
||||
|
||||
def calendar_detail(request, slug):
|
||||
calendar = get_object_or_404(Calendar, slug=slug)
|
||||
|
||||
if not rules.test_rule('calendar_app.view_calendar', request.user, calendar):
|
||||
raise PermissionDenied("У вас нет доступа к этому календарю")
|
||||
|
||||
events = calendar.event_set.all()
|
||||
return render(request, 'calendar_app/calendar.html', {
|
||||
'calendar': calendar,
|
||||
'events': events
|
||||
})
|
||||
|
||||
|
||||
def event_create(request, calendar_slug):
|
||||
calendar = get_object_or_404(Calendar, slug=calendar_slug)
|
||||
|
||||
if not rules.test_rule('calendar_app.create_event', request.user, calendar):
|
||||
return JsonResponse({'error': 'Нет прав на создание событий'}, status=403)
|
||||
|
||||
# Логика создания события
|
||||
event = Event.objects.create(
|
||||
calendar=calendar,
|
||||
creator=request.user,
|
||||
title=request.POST.get('title'),
|
||||
start=request.POST.get('start'),
|
||||
end=request.POST.get('end'),
|
||||
)
|
||||
|
||||
return JsonResponse({'event_id': event.pk})
|
||||
|
||||
|
||||
def event_update(request, pk):
|
||||
event = get_object_or_404(Event, pk=pk)
|
||||
|
||||
if not rules.test_rule('calendar_app.change_event', request.user, event):
|
||||
return JsonResponse({'error': 'Нет прав на изменение'}, status=403)
|
||||
|
||||
event.title = request.POST.get('title', event.title)
|
||||
event.save()
|
||||
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
# В templates:
|
||||
{% load rules %}
|
||||
|
||||
{% has_rule 'calendar_app.create_event' user calendar as can_create %}
|
||||
{% if can_create %}
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#eventModal">
|
||||
Создать событие
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% for event in events %}
|
||||
<div class="event">
|
||||
<h4>{{ event.title }}</h4>
|
||||
<p>{{ event.start|date:"d.m.Y H:i" }} - {{ event.end|date:"d.m.Y H:i" }}</p>
|
||||
|
||||
{% has_rule 'calendar_app.change_event' user event as can_edit %}
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'calendar_app:event_edit' event.pk %}" class="btn btn-sm btn-primary">
|
||||
Редактировать
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% has_rule 'calendar_app.delete_event' user event as can_delete %}
|
||||
{% if can_delete %}
|
||||
<form method="post" action="{% url 'calendar_app:event_delete' event.pk %}"
|
||||
onsubmit="return confirm('Удалить событие?')">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-danger">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
# В DRF permissions (уже реализовано в api/v1/schedule/permissions.py):
|
||||
from rest_framework import permissions
|
||||
import rules
|
||||
|
||||
class IsCalendarOwnerOrAdmin(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Для Calendar
|
||||
if hasattr(obj, 'creator'):
|
||||
calendar = obj
|
||||
# Для Event
|
||||
elif hasattr(obj, 'calendar'):
|
||||
calendar = obj.calendar
|
||||
else:
|
||||
return False
|
||||
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('calendar_app.view_calendar', request.user, calendar)
|
||||
elif request.method in ['POST']:
|
||||
return rules.test_rule('calendar_app.create_event', request.user, calendar)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
if hasattr(obj, 'calendar'): # Event
|
||||
return rules.test_rule('calendar_app.change_event', request.user, obj)
|
||||
else: # Calendar
|
||||
return rules.test_rule('calendar_app.change_calendar', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
if hasattr(obj, 'calendar'):
|
||||
return rules.test_rule('calendar_app.delete_event', request.user, obj)
|
||||
else:
|
||||
return rules.test_rule('calendar_app.delete_calendar', request.user, obj)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Фильтрация QuerySet (только доступные календари):
|
||||
from django.db.models import Q
|
||||
|
||||
def get_accessible_calendars(user):
|
||||
return Calendar.objects.filter(
|
||||
Q(creator=user) | # Собственные календари
|
||||
Q(slug=f'personal-{user.pk}') | # Личный календарь
|
||||
Q(is_public=True) | # Публичные календари
|
||||
Q(department=user.department) | # Календари отдела
|
||||
Q(calendar_type='company') | # Корпоративный календарь
|
||||
Q(subscriptions__user=user) # Подписки
|
||||
).distinct()
|
||||
|
||||
|
||||
# Middleware для защиты iCal экспорта:
|
||||
class CalendarExportMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.path.startswith('/calendar/export/'):
|
||||
# Извлечение календаря
|
||||
slug = request.path.split('/')[-2]
|
||||
calendar = Calendar.objects.filter(slug=slug).first()
|
||||
|
||||
if calendar and not rules.test_rule('calendar_app.export_calendar', request.user, calendar):
|
||||
return HttpResponseForbidden("У вас нет прав на экспорт этого календаря")
|
||||
|
||||
return self.get_response(request)
|
||||
"""
|
||||
@ -9,6 +9,7 @@ class CommunicationsConfig(AppConfig):
|
||||
def ready(self):
|
||||
import communications.signals
|
||||
import communications.notification_signals # Подключаем signals уведомлений
|
||||
import communications.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
def create_main_global_chat(sender, **kwargs):
|
||||
from communications.models import Chat
|
||||
|
||||
370
backend/communications/rules.py
Normal file
370
backend/communications/rules.py
Normal file
@ -0,0 +1,370 @@
|
||||
"""
|
||||
django-rules: декларативные правила доступа для communications (мессенджер)
|
||||
|
||||
Правила используются для проверки permissions на уровне объектов.
|
||||
https://github.com/dfunckt/django-rules
|
||||
"""
|
||||
|
||||
import rules
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРЕДИКАТЫ (predicates)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@rules.predicate
|
||||
def is_superuser(user):
|
||||
"""Суперпользователь имеет все права"""
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_chat_member(user, chat):
|
||||
"""Пользователь является участником чата"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
# Проверка через members/participants
|
||||
if hasattr(chat, 'members'):
|
||||
return user in chat.members.all()
|
||||
|
||||
if hasattr(chat, 'participants'):
|
||||
return user in chat.participants.all()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_chat_owner(user, chat):
|
||||
"""Пользователь является владельцем/создателем чата"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
if hasattr(chat, 'owner'):
|
||||
return chat.owner == user
|
||||
|
||||
if hasattr(chat, 'created_by'):
|
||||
return chat.created_by == user
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_chat_admin(user, chat):
|
||||
"""Пользователь является администратором чата"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
# Проверка через admins
|
||||
if hasattr(chat, 'admins'):
|
||||
return user in chat.admins.all()
|
||||
|
||||
# Проверка через membership с ролью admin
|
||||
if hasattr(chat, 'chatmembership_set'):
|
||||
return chat.chatmembership_set.filter(
|
||||
user=user, role__in=['admin', 'moderator']
|
||||
).exists()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_direct_chat_participant(user, chat):
|
||||
"""Пользователь участвует в личном чате"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
# Проверка типа чата
|
||||
chat_type = getattr(chat, 'chat_type', None) or getattr(chat, 'type', None)
|
||||
if chat_type not in ['direct', 'private', 'personal']:
|
||||
return False
|
||||
|
||||
return is_chat_member(user, chat)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_message_author(user, message):
|
||||
"""Пользователь является автором сообщения"""
|
||||
if message is None:
|
||||
return False
|
||||
|
||||
return message.author == user or message.sender == user
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_access_message_chat(user, message):
|
||||
"""Пользователь имеет доступ к чату, где находится сообщение"""
|
||||
if message is None or not hasattr(message, 'chat'):
|
||||
return False
|
||||
|
||||
return is_chat_member(user, message.chat)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_public_chat(user, chat):
|
||||
"""Чат является публичным (доступен всем)"""
|
||||
if chat is None:
|
||||
return False
|
||||
|
||||
return getattr(chat, 'is_public', False)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_department_chat(user, chat):
|
||||
"""Чат отдела пользователя"""
|
||||
if chat is None or not hasattr(user, 'department'):
|
||||
return False
|
||||
|
||||
if hasattr(chat, 'department'):
|
||||
return chat.department == user.department
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРАВИЛА (rules)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Просмотр чата (доступ к чату)
|
||||
rules.add_rule(
|
||||
'communications.view_chat',
|
||||
is_superuser | is_chat_member | is_public_chat
|
||||
)
|
||||
|
||||
# Отправка сообщений в чат
|
||||
rules.add_rule(
|
||||
'communications.send_message',
|
||||
is_superuser | is_chat_member
|
||||
)
|
||||
|
||||
# Изменение чата (название, описание, настройки)
|
||||
rules.add_rule(
|
||||
'communications.change_chat',
|
||||
is_superuser | is_chat_owner | is_chat_admin
|
||||
)
|
||||
|
||||
# Удаление чата
|
||||
rules.add_rule(
|
||||
'communications.delete_chat',
|
||||
is_superuser | is_chat_owner
|
||||
)
|
||||
|
||||
# Просмотр сообщения
|
||||
rules.add_rule(
|
||||
'communications.view_message',
|
||||
is_superuser | can_access_message_chat
|
||||
)
|
||||
|
||||
# Изменение сообщения (редактирование)
|
||||
rules.add_rule(
|
||||
'communications.change_message',
|
||||
is_superuser | is_message_author
|
||||
)
|
||||
|
||||
# Удаление сообщения
|
||||
rules.add_rule(
|
||||
'communications.delete_message',
|
||||
is_superuser | is_message_author | is_chat_admin
|
||||
)
|
||||
|
||||
# Добавление участников в чат
|
||||
rules.add_rule(
|
||||
'communications.add_members',
|
||||
is_superuser | is_chat_owner | is_chat_admin
|
||||
)
|
||||
|
||||
# Удаление участников из чата
|
||||
rules.add_rule(
|
||||
'communications.remove_members',
|
||||
is_superuser | is_chat_owner | is_chat_admin
|
||||
)
|
||||
|
||||
# Закрепление сообщений
|
||||
rules.add_rule(
|
||||
'communications.pin_message',
|
||||
is_superuser | is_chat_owner | is_chat_admin
|
||||
)
|
||||
|
||||
# Изменение ролей участников
|
||||
rules.add_rule(
|
||||
'communications.change_member_role',
|
||||
is_superuser | is_chat_owner
|
||||
)
|
||||
|
||||
# Выход из чата (покинуть чат)
|
||||
rules.add_rule(
|
||||
'communications.leave_chat',
|
||||
is_chat_member & ~is_chat_owner # Все кроме владельца
|
||||
)
|
||||
|
||||
# Просмотр истории чата
|
||||
rules.add_rule(
|
||||
'communications.view_chat_history',
|
||||
is_superuser | is_chat_member
|
||||
)
|
||||
|
||||
# Поиск по чатам/сообщениям
|
||||
rules.add_rule(
|
||||
'communications.search_messages',
|
||||
rules.is_authenticated # Все авторизованные
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ИСПОЛЬЗОВАНИЕ В КОДЕ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
# В views:
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import rules
|
||||
|
||||
def chat_detail(request, pk):
|
||||
chat = get_object_or_404(Chat, pk=pk)
|
||||
|
||||
if not rules.test_rule('communications.view_chat', request.user, chat):
|
||||
raise PermissionDenied("У вас нет доступа к этому чату")
|
||||
|
||||
messages = chat.messages.all()
|
||||
return render(request, 'communications/chat.html', {
|
||||
'chat': chat,
|
||||
'messages': messages
|
||||
})
|
||||
|
||||
|
||||
def send_message(request, chat_pk):
|
||||
chat = get_object_or_404(Chat, pk=chat_pk)
|
||||
|
||||
if not rules.test_rule('communications.send_message', request.user, chat):
|
||||
return JsonResponse({'error': 'Нет прав на отправку сообщений'}, status=403)
|
||||
|
||||
message = Message.objects.create(
|
||||
chat=chat,
|
||||
author=request.user,
|
||||
text=request.POST.get('text')
|
||||
)
|
||||
|
||||
return JsonResponse({'message_id': message.pk})
|
||||
|
||||
|
||||
def delete_message(request, pk):
|
||||
message = get_object_or_404(Message, pk=pk)
|
||||
|
||||
if not rules.test_rule('communications.delete_message', request.user, message):
|
||||
return JsonResponse({'error': 'Нет прав на удаление'}, status=403)
|
||||
|
||||
message.delete()
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
# В templates:
|
||||
{% load rules %}
|
||||
|
||||
{% has_rule 'communications.send_message' user chat as can_send %}
|
||||
{% if can_send %}
|
||||
<form method="post" action="{% url 'communications:send' chat.pk %}" id="message-form">
|
||||
{% csrf_token %}
|
||||
<textarea name="text" class="form-control" placeholder="Введите сообщение..."></textarea>
|
||||
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-muted">Вы не можете отправлять сообщения в этот чат</p>
|
||||
{% endif %}
|
||||
|
||||
{% for message in messages %}
|
||||
<div class="message">
|
||||
<strong>{{ message.author }}</strong>: {{ message.text }}
|
||||
|
||||
{% has_rule 'communications.delete_message' user message as can_delete %}
|
||||
{% if can_delete %}
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteMessage({{ message.pk }})">
|
||||
Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
# В DRF permissions:
|
||||
from rest_framework import permissions
|
||||
import rules
|
||||
|
||||
class ChatPermission(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('communications.view_chat', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('communications.change_chat', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('communications.delete_chat', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
class MessagePermission(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('communications.view_message', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('communications.change_message', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('communications.delete_message', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
# В WebSocket consumer:
|
||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||
import rules
|
||||
|
||||
class ChatConsumer(AsyncJsonWebsocketConsumer):
|
||||
async def connect(self):
|
||||
chat_id = self.scope['url_route']['kwargs']['chat_id']
|
||||
chat = await database_sync_to_async(Chat.objects.get)(pk=chat_id)
|
||||
|
||||
# Проверка доступа
|
||||
if not await database_sync_to_async(rules.test_rule)(
|
||||
'communications.view_chat', self.scope['user'], chat
|
||||
):
|
||||
await self.close()
|
||||
return
|
||||
|
||||
await self.accept()
|
||||
# Добавляем в группу чата
|
||||
await self.channel_layer.group_add(f'chat_{chat_id}', self.channel_name)
|
||||
|
||||
async def receive_json(self, content):
|
||||
action = content.get('action')
|
||||
|
||||
if action == 'send_message':
|
||||
chat = await database_sync_to_async(Chat.objects.get)(pk=self.chat_id)
|
||||
|
||||
# Проверка прав на отправку
|
||||
if not await database_sync_to_async(rules.test_rule)(
|
||||
'communications.send_message', self.scope['user'], chat
|
||||
):
|
||||
await self.send_json({'error': 'Permission denied'})
|
||||
return
|
||||
|
||||
# Сохранение сообщения
|
||||
message = await database_sync_to_async(Message.objects.create)(
|
||||
chat=chat,
|
||||
author=self.scope['user'],
|
||||
text=content['text']
|
||||
)
|
||||
|
||||
# Рассылка всем участникам
|
||||
await self.channel_layer.group_send(
|
||||
f'chat_{chat.pk}',
|
||||
{'type': 'chat.message', 'message': message.to_dict()}
|
||||
)
|
||||
|
||||
|
||||
# Фильтрация QuerySet (только доступные чаты):
|
||||
from django.db.models import Q
|
||||
|
||||
def get_accessible_chats(user):
|
||||
return Chat.objects.filter(
|
||||
Q(members=user) | # Участник чата
|
||||
Q(is_public=True) | # Публичный чат
|
||||
Q(department=user.department) # Чат отдела
|
||||
).distinct()
|
||||
"""
|
||||
@ -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
296
backend/documents/rules.py
Normal 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)
|
||||
"""
|
||||
@ -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
186
backend/employees/rules.py
Normal 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)
|
||||
"""
|
||||
@ -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 бэкенд
|
||||
]
|
||||
|
||||
|
||||
@ -8,3 +8,4 @@ class FeedConfig(AppConfig):
|
||||
def ready(self):
|
||||
# Подключаем signals для уведомлений
|
||||
import feed.notification_signals # noqa
|
||||
import feed.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
386
backend/feed/rules.py
Normal file
386
backend/feed/rules.py
Normal file
@ -0,0 +1,386 @@
|
||||
"""
|
||||
django-rules: декларативные правила доступа для feed (лента новостей)
|
||||
|
||||
Правила используются для проверки permissions на уровне объектов.
|
||||
https://github.com/dfunckt/django-rules
|
||||
"""
|
||||
|
||||
import rules
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРЕДИКАТЫ (predicates)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@rules.predicate
|
||||
def is_superuser(user):
|
||||
"""Суперпользователь имеет все права"""
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_post_author(user, post):
|
||||
"""Пользователь является автором поста"""
|
||||
if post is None:
|
||||
return False
|
||||
|
||||
return post.author == user or post.created_by == user
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_publish_posts(user):
|
||||
"""
|
||||
Пользователь может публиковать посты в ленте.
|
||||
Адаптируйте под вашу логику (по должности, группе и т.д.)
|
||||
"""
|
||||
if not hasattr(user, 'position'):
|
||||
return False
|
||||
|
||||
position_name = getattr(user.position, 'name', '').lower()
|
||||
|
||||
# Руководители, PR-менеджеры, секретари могут публиковать
|
||||
return any(keyword in position_name for keyword in [
|
||||
'руководитель', 'начальник', 'директор', 'секретарь',
|
||||
'pr', 'маркетинг', 'коммуникац'
|
||||
])
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_moderate_posts(user):
|
||||
"""Пользователь может модерировать посты (удалять чужие, редактировать)"""
|
||||
return can_publish_posts(user) or is_superuser(user)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_public_post(user, post):
|
||||
"""Пост является публичным (доступен всем)"""
|
||||
if post is None:
|
||||
return False
|
||||
|
||||
return getattr(post, 'is_public', True)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_department_post(user, post):
|
||||
"""Пост относится к отделу пользователя"""
|
||||
if post is None or not hasattr(user, 'department'):
|
||||
return False
|
||||
|
||||
if hasattr(post, 'department'):
|
||||
return post.department == user.department
|
||||
|
||||
if hasattr(post, 'target_departments'):
|
||||
return user.department in post.target_departments.all()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_comment_author(user, comment):
|
||||
"""Пользователь является автором комментария"""
|
||||
if comment is None:
|
||||
return False
|
||||
|
||||
return comment.author == user or comment.created_by == user
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_access_comment_post(user, comment):
|
||||
"""Пользователь имеет доступ к посту, где находится комментарий"""
|
||||
if comment is None or not hasattr(comment, 'post'):
|
||||
return False
|
||||
|
||||
post = comment.post
|
||||
return is_public_post(user, post) or is_department_post(user, post)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_post_pinned(user, post):
|
||||
"""Пост закреплён (может видеть любой сотрудник)"""
|
||||
if post is None:
|
||||
return False
|
||||
|
||||
return getattr(post, 'is_pinned', False)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРАВИЛА (rules)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Просмотр поста
|
||||
rules.add_rule(
|
||||
'feed.view_post',
|
||||
is_superuser | is_public_post | is_department_post | is_post_pinned |
|
||||
is_post_author # Автор всегда видит свой пост, даже если он не опубликован
|
||||
)
|
||||
|
||||
# Создание поста
|
||||
rules.add_rule(
|
||||
'feed.create_post',
|
||||
is_superuser | can_publish_posts
|
||||
)
|
||||
|
||||
# Изменение поста
|
||||
rules.add_rule(
|
||||
'feed.change_post',
|
||||
is_superuser | is_post_author | can_moderate_posts
|
||||
)
|
||||
|
||||
# Удаление поста
|
||||
rules.add_rule(
|
||||
'feed.delete_post',
|
||||
is_superuser | is_post_author | can_moderate_posts
|
||||
)
|
||||
|
||||
# Публикация поста (изменение статуса draft → published)
|
||||
rules.add_rule(
|
||||
'feed.publish_post',
|
||||
is_superuser | (is_post_author & can_publish_posts) | can_moderate_posts
|
||||
)
|
||||
|
||||
# Закрепление поста
|
||||
rules.add_rule(
|
||||
'feed.pin_post',
|
||||
is_superuser | can_moderate_posts
|
||||
)
|
||||
|
||||
# Добавление комментария к посту
|
||||
rules.add_rule(
|
||||
'feed.comment_post',
|
||||
rules.is_authenticated # Любой авторизованный может комментировать
|
||||
)
|
||||
|
||||
# Просмотр комментария
|
||||
rules.add_rule(
|
||||
'feed.view_comment',
|
||||
is_superuser | can_access_comment_post
|
||||
)
|
||||
|
||||
# Изменение комментария
|
||||
rules.add_rule(
|
||||
'feed.change_comment',
|
||||
is_superuser | is_comment_author
|
||||
)
|
||||
|
||||
# Удаление комментария
|
||||
rules.add_rule(
|
||||
'feed.delete_comment',
|
||||
is_superuser | is_comment_author | can_moderate_posts
|
||||
)
|
||||
|
||||
# Лайк/реакция на пост
|
||||
rules.add_rule(
|
||||
'feed.react_to_post',
|
||||
rules.is_authenticated # Любой авторизованный может ставить реакции
|
||||
)
|
||||
|
||||
# Просмотр черновиков (draft posts)
|
||||
rules.add_rule(
|
||||
'feed.view_drafts',
|
||||
is_superuser | can_publish_posts
|
||||
)
|
||||
|
||||
# Просмотр статистики по постам
|
||||
rules.add_rule(
|
||||
'feed.view_statistics',
|
||||
is_superuser | can_publish_posts
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ИСПОЛЬЗОВАНИЕ В КОДЕ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
# В views:
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import rules
|
||||
|
||||
def post_detail(request, pk):
|
||||
post = get_object_or_404(Post, pk=pk)
|
||||
|
||||
if not rules.test_rule('feed.view_post', request.user, post):
|
||||
raise PermissionDenied("У вас нет доступа к этому посту")
|
||||
|
||||
comments = post.comments.all()
|
||||
return render(request, 'feed/post_detail.html', {
|
||||
'post': post,
|
||||
'comments': comments
|
||||
})
|
||||
|
||||
|
||||
def post_create(request):
|
||||
if not rules.test_rule('feed.create_post', request.user, None):
|
||||
raise PermissionDenied("У вас нет прав на создание постов")
|
||||
|
||||
if request.method == 'POST':
|
||||
post = Post.objects.create(
|
||||
author=request.user,
|
||||
title=request.POST.get('title'),
|
||||
content=request.POST.get('content'),
|
||||
status='draft'
|
||||
)
|
||||
return redirect('feed:post_detail', pk=post.pk)
|
||||
|
||||
return render(request, 'feed/post_create.html')
|
||||
|
||||
|
||||
def post_publish(request, pk):
|
||||
post = get_object_or_404(Post, pk=pk)
|
||||
|
||||
if not rules.test_rule('feed.publish_post', request.user, post):
|
||||
return JsonResponse({'error': 'Нет прав на публикацию'}, status=403)
|
||||
|
||||
post.status = 'published'
|
||||
post.published_at = timezone.now()
|
||||
post.save()
|
||||
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
def add_comment(request, post_pk):
|
||||
post = get_object_or_404(Post, pk=post_pk)
|
||||
|
||||
if not rules.test_rule('feed.comment_post', request.user, None):
|
||||
return JsonResponse({'error': 'Нет прав на комментирование'}, status=403)
|
||||
|
||||
comment = Comment.objects.create(
|
||||
post=post,
|
||||
author=request.user,
|
||||
text=request.POST.get('text')
|
||||
)
|
||||
|
||||
return JsonResponse({'comment_id': comment.pk})
|
||||
|
||||
|
||||
# В templates:
|
||||
{% load rules %}
|
||||
|
||||
{# Список постов #}
|
||||
{% for post in posts %}
|
||||
{% has_rule 'feed.view_post' user post as can_view %}
|
||||
{% if can_view %}
|
||||
<div class="post card mb-3">
|
||||
<div class="card-body">
|
||||
<h3>{{ post.title }}</h3>
|
||||
<p>{{ post.content|truncatewords:50 }}</p>
|
||||
<p class="text-muted">Автор: {{ post.author }}, {{ post.created_at|date:"d.m.Y H:i" }}</p>
|
||||
|
||||
<a href="{% url 'feed:post_detail' post.pk %}" class="btn btn-primary">
|
||||
Читать далее
|
||||
</a>
|
||||
|
||||
{% has_rule 'feed.change_post' user post as can_edit %}
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'feed:post_edit' post.pk %}" class="btn btn-secondary">
|
||||
Редактировать
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% has_rule 'feed.delete_post' user post as can_delete %}
|
||||
{% if can_delete %}
|
||||
<form method="post" action="{% url 'feed:post_delete' post.pk %}"
|
||||
style="display:inline" onsubmit="return confirm('Удалить пост?')">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% has_rule 'feed.pin_post' user post as can_pin %}
|
||||
{% if can_pin and not post.is_pinned %}
|
||||
<form method="post" action="{% url 'feed:post_pin' post.pk %}" style="display:inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-warning">Закрепить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Создание поста #}
|
||||
{% has_rule 'feed.create_post' user None as can_create %}
|
||||
{% if can_create %}
|
||||
<a href="{% url 'feed:post_create' %}" class="btn btn-success mb-3">
|
||||
Создать пост
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# Комментарии #}
|
||||
{% has_rule 'feed.comment_post' user None as can_comment %}
|
||||
{% if can_comment %}
|
||||
<form method="post" action="{% url 'feed:add_comment' post.pk %}">
|
||||
{% csrf_token %}
|
||||
<textarea name="text" class="form-control mb-2" placeholder="Ваш комментарий..."></textarea>
|
||||
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
# В DRF permissions:
|
||||
from rest_framework import permissions
|
||||
import rules
|
||||
|
||||
class PostPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return rules.test_rule('feed.create_post', request.user, None)
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('feed.view_post', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('feed.change_post', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('feed.delete_post', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
class CommentPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return rules.test_rule('feed.comment_post', request.user, None)
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('feed.view_comment', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('feed.change_comment', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('feed.delete_comment', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
# Фильтрация QuerySet (только доступные посты):
|
||||
from django.db.models import Q
|
||||
|
||||
def get_accessible_posts(user):
|
||||
return Post.objects.filter(
|
||||
Q(status='published') & ( # Только опубликованные И (публичные ИЛИ для отдела)
|
||||
Q(is_public=True) |
|
||||
Q(department=user.department) |
|
||||
Q(target_departments=user.department)
|
||||
) |
|
||||
Q(author=user) # + собственные посты (включая черновики)
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
|
||||
# API endpoint для лайков:
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def like_post(request, pk):
|
||||
post = get_object_or_404(Post, pk=pk)
|
||||
|
||||
if not rules.test_rule('feed.react_to_post', request.user, None):
|
||||
return Response({'error': 'Permission denied'}, status=403)
|
||||
|
||||
like, created = PostLike.objects.get_or_create(post=post, user=request.user)
|
||||
|
||||
if not created:
|
||||
like.delete()
|
||||
return Response({'liked': False, 'likes_count': post.likes.count()})
|
||||
|
||||
return Response({'liked': True, 'likes_count': post.likes.count()})
|
||||
"""
|
||||
@ -9,3 +9,4 @@ class ProcurementConfig(AppConfig):
|
||||
def ready(self):
|
||||
"""Подключение сигналов при загрузке приложения."""
|
||||
import procurement.signals # noqa: F401
|
||||
import procurement.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
354
backend/procurement/rules.py
Normal file
354
backend/procurement/rules.py
Normal file
@ -0,0 +1,354 @@
|
||||
"""
|
||||
django-rules: декларативные правила доступа для procurement (закупки)
|
||||
|
||||
Правила используются для проверки permissions на уровне объектов.
|
||||
https://github.com/dfunckt/django-rules
|
||||
"""
|
||||
|
||||
import rules
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРЕДИКАТЫ (predicates)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@rules.predicate
|
||||
def is_superuser(user):
|
||||
"""Суперпользователь имеет все права"""
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_procurement_manager(user):
|
||||
"""
|
||||
Пользователь является менеджером по закупкам.
|
||||
Адаптируйте под вашу логику (по должности, группе и т.д.)
|
||||
"""
|
||||
if not hasattr(user, 'position'):
|
||||
return False
|
||||
|
||||
position_name = getattr(user.position, 'name', '').lower()
|
||||
return any(keyword in position_name for keyword in [
|
||||
'закупк', 'снабжен', 'procurement', 'supply'
|
||||
])
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_purchase_request_author(user, purchase_request):
|
||||
"""Пользователь является автором заявки на закупку"""
|
||||
if purchase_request is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
purchase_request.created_by == user or
|
||||
getattr(purchase_request, 'author', None) == user
|
||||
)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_purchase_request_approver(user, purchase_request):
|
||||
"""Пользователь назначен согласующим для заявки на закупку"""
|
||||
if purchase_request is None:
|
||||
return False
|
||||
|
||||
# Проверка через approvers
|
||||
if hasattr(purchase_request, 'approvers'):
|
||||
return user in purchase_request.approvers.all()
|
||||
|
||||
# Проверка через approval_chain
|
||||
if hasattr(purchase_request, 'approval_chain'):
|
||||
return purchase_request.approval_chain.filter(approver=user).exists()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_department_purchase_request(user, purchase_request):
|
||||
"""Заявка на закупку относится к отделу пользователя"""
|
||||
if purchase_request is None or not hasattr(user, 'department'):
|
||||
return False
|
||||
|
||||
if hasattr(purchase_request, 'department'):
|
||||
return purchase_request.department == user.department
|
||||
|
||||
# Проверка через автора
|
||||
if hasattr(purchase_request, 'created_by') and hasattr(purchase_request.created_by, 'department'):
|
||||
return purchase_request.created_by.department == user.department
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_approve_purchases(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_supplier_contact(user, supplier):
|
||||
"""Пользователь является контактным лицом поставщика"""
|
||||
if supplier is None:
|
||||
return False
|
||||
|
||||
if hasattr(supplier, 'contact_person'):
|
||||
return supplier.contact_person == user
|
||||
|
||||
if hasattr(supplier, 'managers'):
|
||||
return user in supplier.managers.all()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_contract_manager(user, contract):
|
||||
"""Пользователь является менеджером договора"""
|
||||
if contract is None:
|
||||
return False
|
||||
|
||||
if hasattr(contract, 'manager'):
|
||||
return contract.manager == user
|
||||
|
||||
if hasattr(contract, 'responsible_person'):
|
||||
return contract.responsible_person == user
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_manage_suppliers(user):
|
||||
"""Пользователь может управлять поставщиками"""
|
||||
return is_procurement_manager(user) or is_superuser(user)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРАВИЛА (rules)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Просмотр заявки на закупку
|
||||
rules.add_rule(
|
||||
'procurement.view_purchase_request',
|
||||
is_superuser | is_purchase_request_author | is_procurement_manager |
|
||||
is_purchase_request_approver | can_approve_purchases
|
||||
)
|
||||
|
||||
# Создание заявки на закупку
|
||||
rules.add_rule(
|
||||
'procurement.create_purchase_request',
|
||||
rules.is_authenticated # Любой сотрудник может создать заявку
|
||||
)
|
||||
|
||||
# Изменение заявки на закупку
|
||||
rules.add_rule(
|
||||
'procurement.change_purchase_request',
|
||||
is_superuser | is_purchase_request_author | is_procurement_manager
|
||||
)
|
||||
|
||||
# Удаление заявки на закупку
|
||||
rules.add_rule(
|
||||
'procurement.delete_purchase_request',
|
||||
is_superuser | is_purchase_request_author
|
||||
)
|
||||
|
||||
# Согласование заявки на закупку
|
||||
rules.add_rule(
|
||||
'procurement.approve_purchase_request',
|
||||
is_superuser | is_purchase_request_approver | can_approve_purchases | is_procurement_manager
|
||||
)
|
||||
|
||||
# Изменение статуса заявки
|
||||
rules.add_rule(
|
||||
'procurement.change_request_status',
|
||||
is_superuser | is_procurement_manager | can_approve_purchases
|
||||
)
|
||||
|
||||
# Просмотр поставщика
|
||||
rules.add_rule(
|
||||
'procurement.view_supplier',
|
||||
is_superuser | is_procurement_manager | is_supplier_contact
|
||||
)
|
||||
|
||||
# Создание/изменение поставщика
|
||||
rules.add_rule(
|
||||
'procurement.manage_supplier',
|
||||
is_superuser | can_manage_suppliers
|
||||
)
|
||||
|
||||
# Удаление поставщика
|
||||
rules.add_rule(
|
||||
'procurement.delete_supplier',
|
||||
is_superuser
|
||||
)
|
||||
|
||||
# Просмотр договора
|
||||
rules.add_rule(
|
||||
'procurement.view_contract',
|
||||
is_superuser | is_contract_manager | is_procurement_manager
|
||||
)
|
||||
|
||||
# Создание/изменение договора
|
||||
rules.add_rule(
|
||||
'procurement.manage_contract',
|
||||
is_superuser | is_contract_manager | can_manage_suppliers
|
||||
)
|
||||
|
||||
# Удаление договора
|
||||
rules.add_rule(
|
||||
'procurement.delete_contract',
|
||||
is_superuser
|
||||
)
|
||||
|
||||
# Просмотр всех заявок (для менеджеров)
|
||||
rules.add_rule(
|
||||
'procurement.view_all_requests',
|
||||
is_superuser | is_procurement_manager | can_approve_purchases
|
||||
)
|
||||
|
||||
# Просмотр отчётов по закупкам
|
||||
rules.add_rule(
|
||||
'procurement.view_reports',
|
||||
is_superuser | is_procurement_manager | can_approve_purchases
|
||||
)
|
||||
|
||||
# Экспорт данных по закупкам
|
||||
rules.add_rule(
|
||||
'procurement.export_data',
|
||||
is_superuser | is_procurement_manager
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ИСПОЛЬЗОВАНИЕ В КОДЕ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
# В views:
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import rules
|
||||
|
||||
def purchase_request_detail(request, pk):
|
||||
purchase_request = get_object_or_404(PurchaseRequest, pk=pk)
|
||||
|
||||
if not rules.test_rule('procurement.view_purchase_request', request.user, purchase_request):
|
||||
raise PermissionDenied("У вас нет доступа к этой заявке")
|
||||
|
||||
return render(request, 'procurement/request_detail.html', {
|
||||
'request': purchase_request
|
||||
})
|
||||
|
||||
|
||||
def approve_purchase_request(request, pk):
|
||||
purchase_request = get_object_or_404(PurchaseRequest, pk=pk)
|
||||
|
||||
if not rules.test_rule('procurement.approve_purchase_request', request.user, purchase_request):
|
||||
return JsonResponse({'error': 'Нет прав на согласование'}, status=403)
|
||||
|
||||
purchase_request.status = 'approved'
|
||||
purchase_request.approved_by = request.user
|
||||
purchase_request.approved_at = timezone.now()
|
||||
purchase_request.save()
|
||||
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
def supplier_list(request):
|
||||
# Фильтрация: только доступные поставщики
|
||||
if rules.test_rule('procurement.view_all_requests', request.user, None):
|
||||
suppliers = Supplier.objects.all()
|
||||
else:
|
||||
suppliers = Supplier.objects.filter(
|
||||
Q(contact_person=request.user) | Q(managers=request.user)
|
||||
)
|
||||
|
||||
return render(request, 'procurement/suppliers.html', {'suppliers': suppliers})
|
||||
|
||||
|
||||
# В templates:
|
||||
{% load rules %}
|
||||
|
||||
{% has_rule 'procurement.approve_purchase_request' user purchase_request as can_approve %}
|
||||
{% if can_approve and purchase_request.status == 'pending' %}
|
||||
<form method="post" action="{% url 'procurement:approve' purchase_request.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-success">Согласовать</button>
|
||||
</form>
|
||||
<form method="post" action="{% url 'procurement:reject' purchase_request.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Отклонить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% has_rule 'procurement.manage_supplier' user None as can_manage %}
|
||||
{% if can_manage %}
|
||||
<a href="{% url 'procurement:supplier_create' %}" class="btn btn-primary">
|
||||
Добавить поставщика
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
# В DRF permissions:
|
||||
from rest_framework import permissions
|
||||
import rules
|
||||
|
||||
class PurchaseRequestPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return rules.test_rule('procurement.create_purchase_request', request.user, None)
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('procurement.view_purchase_request', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('procurement.change_purchase_request', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('procurement.delete_purchase_request', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
class SupplierPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return rules.test_rule('procurement.manage_supplier', request.user, None)
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('procurement.view_supplier', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('procurement.manage_supplier', request.user, None)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('procurement.delete_supplier', request.user, None)
|
||||
return False
|
||||
|
||||
|
||||
# Фильтрация QuerySet:
|
||||
from django.db.models import Q
|
||||
|
||||
def get_accessible_purchase_requests(user):
|
||||
if rules.test_rule('procurement.view_all_requests', user, None):
|
||||
return PurchaseRequest.objects.all()
|
||||
|
||||
return PurchaseRequest.objects.filter(
|
||||
Q(created_by=user) | # Созданные пользователем
|
||||
Q(approvers=user) | # Требующие согласования
|
||||
Q(department=user.department) # Заявки отдела
|
||||
).distinct()
|
||||
|
||||
|
||||
def get_accessible_suppliers(user):
|
||||
if rules.test_rule('procurement.view_all_requests', user, None):
|
||||
return Supplier.objects.all()
|
||||
|
||||
return Supplier.objects.filter(
|
||||
Q(contact_person=user) | Q(managers=user)
|
||||
).distinct()
|
||||
"""
|
||||
@ -8,3 +8,4 @@ class RequestsAppConfig(AppConfig):
|
||||
def ready(self):
|
||||
# Подключаем signals для уведомлений
|
||||
import requests_app.notification_signals # noqa
|
||||
import requests_app.rules # django-rules: регистрация предикатов и правил доступа
|
||||
|
||||
270
backend/requests_app/rules.py
Normal file
270
backend/requests_app/rules.py
Normal file
@ -0,0 +1,270 @@
|
||||
"""
|
||||
django-rules: декларативные правила доступа для requests_app
|
||||
|
||||
Правила используются для проверки permissions на уровне объектов.
|
||||
https://github.com/dfunckt/django-rules
|
||||
"""
|
||||
|
||||
import rules
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРЕДИКАТЫ (predicates)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@rules.predicate
|
||||
def is_superuser(user):
|
||||
"""Суперпользователь имеет все права"""
|
||||
return user.is_superuser
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_request_author(user, request_obj):
|
||||
"""Пользователь является автором заявки"""
|
||||
if request_obj is None:
|
||||
return False
|
||||
return request_obj.created_by == user or request_obj.author == user
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_request_assignee(user, request_obj):
|
||||
"""Пользователь является исполнителем заявки"""
|
||||
if request_obj is None:
|
||||
return False
|
||||
|
||||
# Проверка через assigned_to (один исполнитель)
|
||||
if hasattr(request_obj, 'assigned_to'):
|
||||
return request_obj.assigned_to == user
|
||||
|
||||
# Проверка через assignees (несколько исполнителей)
|
||||
if hasattr(request_obj, 'assignees'):
|
||||
return user in request_obj.assignees.all()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_request_approver(user, request_obj):
|
||||
"""Пользователь назначен согласующим для этой заявки"""
|
||||
if request_obj is None:
|
||||
return False
|
||||
|
||||
# Проверка через approvers
|
||||
if hasattr(request_obj, 'approvers'):
|
||||
return user in request_obj.approvers.all()
|
||||
|
||||
# Проверка через approval_chain
|
||||
if hasattr(request_obj, 'approval_chain'):
|
||||
return request_obj.approval_chain.filter(approver=user).exists()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_manage_requests(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_department_request(user, request_obj):
|
||||
"""Заявка относится к отделу пользователя"""
|
||||
if request_obj is None or not hasattr(user, 'department'):
|
||||
return False
|
||||
|
||||
# Проверка через department
|
||||
if hasattr(request_obj, 'department'):
|
||||
return request_obj.department == user.department
|
||||
|
||||
# Проверка через автора заявки
|
||||
if hasattr(request_obj, 'created_by') and hasattr(request_obj.created_by, 'department'):
|
||||
return request_obj.created_by.department == user.department
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_request_watcher(user, request_obj):
|
||||
"""Пользователь наблюдает за заявкой (подписан на уведомления)"""
|
||||
if request_obj is None:
|
||||
return False
|
||||
|
||||
if hasattr(request_obj, 'watchers'):
|
||||
return user in request_obj.watchers.all()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def can_change_request_status(user, request_obj):
|
||||
"""Пользователь может менять статус заявки"""
|
||||
if request_obj is None:
|
||||
return False
|
||||
|
||||
# Автор может закрывать/отменять свою заявку
|
||||
if is_request_author(user, request_obj):
|
||||
return True
|
||||
|
||||
# Исполнитель может переводить в работу/выполнено
|
||||
if is_request_assignee(user, request_obj):
|
||||
return True
|
||||
|
||||
# Менеджеры могут менять любые статусы
|
||||
if can_manage_requests(user):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ПРАВИЛА (rules)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Просмотр заявки
|
||||
rules.add_rule(
|
||||
'requests_app.view_request',
|
||||
is_superuser | is_request_author | is_request_assignee |
|
||||
is_request_approver | is_request_watcher | can_manage_requests
|
||||
)
|
||||
|
||||
# Изменение заявки
|
||||
rules.add_rule(
|
||||
'requests_app.change_request',
|
||||
is_superuser | is_request_author | can_manage_requests
|
||||
)
|
||||
|
||||
# Удаление заявки (только автор и менеджеры)
|
||||
rules.add_rule(
|
||||
'requests_app.delete_request',
|
||||
is_superuser | (is_request_author & rules.always_allow) | can_manage_requests
|
||||
)
|
||||
|
||||
# Назначение исполнителя
|
||||
rules.add_rule(
|
||||
'requests_app.assign_request',
|
||||
is_superuser | can_manage_requests | is_request_author
|
||||
)
|
||||
|
||||
# Изменение статуса заявки
|
||||
rules.add_rule(
|
||||
'requests_app.change_status',
|
||||
is_superuser | can_change_request_status
|
||||
)
|
||||
|
||||
# Согласование заявки
|
||||
rules.add_rule(
|
||||
'requests_app.approve_request',
|
||||
is_superuser | is_request_approver | can_manage_requests
|
||||
)
|
||||
|
||||
# Комментирование заявки
|
||||
rules.add_rule(
|
||||
'requests_app.comment_request',
|
||||
is_superuser | is_request_author | is_request_assignee |
|
||||
is_request_watcher | can_manage_requests
|
||||
)
|
||||
|
||||
# Добавление наблюдателей
|
||||
rules.add_rule(
|
||||
'requests_app.add_watchers',
|
||||
is_superuser | is_request_author | can_manage_requests
|
||||
)
|
||||
|
||||
# Просмотр всех заявок отдела
|
||||
rules.add_rule(
|
||||
'requests_app.view_department_requests',
|
||||
is_superuser | can_manage_requests
|
||||
)
|
||||
|
||||
# Просмотр статистики по заявкам
|
||||
rules.add_rule(
|
||||
'requests_app.view_statistics',
|
||||
is_superuser | can_manage_requests
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ИСПОЛЬЗОВАНИЕ В КОДЕ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
# В views:
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import rules
|
||||
|
||||
def request_detail(request, pk):
|
||||
request_obj = get_object_or_404(Request, pk=pk)
|
||||
|
||||
if not rules.test_rule('requests_app.view_request', request.user, request_obj):
|
||||
raise PermissionDenied
|
||||
|
||||
return render(request, 'requests_app/detail.html', {'request': request_obj})
|
||||
|
||||
|
||||
def request_assign(request, pk):
|
||||
request_obj = get_object_or_404(Request, pk=pk)
|
||||
|
||||
if not rules.test_rule('requests_app.assign_request', request.user, request_obj):
|
||||
raise PermissionDenied
|
||||
|
||||
# Логика назначения
|
||||
assignee_id = request.POST.get('assignee_id')
|
||||
request_obj.assigned_to_id = assignee_id
|
||||
request_obj.save()
|
||||
|
||||
return redirect('requests_app:detail', pk=request_obj.pk)
|
||||
|
||||
|
||||
# В templates:
|
||||
{% load rules %}
|
||||
|
||||
{% has_rule 'requests_app.change_status' user request as can_change_status %}
|
||||
{% if can_change_status %}
|
||||
<form method="post" action="{% url 'requests_app:change_status' request.pk %}">
|
||||
{% csrf_token %}
|
||||
<select name="status" class="form-select">
|
||||
<option value="new">Новая</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="completed">Выполнено</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Изменить статус</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
# В DRF permissions:
|
||||
from rest_framework import permissions
|
||||
import rules
|
||||
|
||||
class RequestPermission(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return rules.test_rule('requests_app.view_request', request.user, obj)
|
||||
elif request.method in ['PUT', 'PATCH']:
|
||||
return rules.test_rule('requests_app.change_request', request.user, obj)
|
||||
elif request.method == 'DELETE':
|
||||
return rules.test_rule('requests_app.delete_request', request.user, obj)
|
||||
return False
|
||||
|
||||
|
||||
# Фильтрация QuerySet (только доступные заявки):
|
||||
from django.db.models import Q
|
||||
|
||||
def get_accessible_requests(user):
|
||||
return Request.objects.filter(
|
||||
Q(created_by=user) | # Созданные пользователем
|
||||
Q(assigned_to=user) | # Назначенные пользователю
|
||||
Q(watchers=user) | # Наблюдаемые пользователем
|
||||
Q(approvers=user) | # Требующие согласования
|
||||
Q(department=user.department) # Заявки отдела
|
||||
).distinct()
|
||||
"""
|
||||
@ -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
|
||||
|
||||
527
docs/guides/DJANGO_RULES_INTEGRATION.md
Normal file
527
docs/guides/DJANGO_RULES_INTEGRATION.md
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user