igor_nadein 3e672b2edc feat: добавлены правила для 5 приложений (requests, communications, calendar, procurement, feed)
- requests_app/rules.py: 8 предикатов + 10 правил
  * Предикаты: is_request_author, is_request_assignee, can_manage_requests и др.
  * Правила: view/change/delete/assign/approve requests, комментирование, статистика

- communications/rules.py: 9 предикатов + 15 правил
  * Предикаты: is_chat_member, is_chat_owner, is_chat_admin, is_message_author и др.
  * Правила: view/send/change/delete чаты и сообщения, управление участниками, закрепление
  * Включены примеры для WebSocket consumers

- calendar_app/rules.py: 9 предикатов + 12 правил
  * Предикаты: is_calendar_owner, is_personal_calendar, has_calendar_access и др.
  * Правила: view/change/delete календари и события, подписки, экспорт iCal, приглашения

- procurement/rules.py: 8 предикатов + 13 правил
  * Предикаты: is_procurement_manager, is_purchase_request_author, can_approve_purchases и др.
  * Правила: view/change/delete заявки на закупку, управление поставщиками и договорами

- feed/rules.py: 8 предикатов + 11 правил
  * Предикаты: is_post_author, can_publish_posts, can_moderate_posts и др.
  * Правила: view/create/change/delete посты, публикация, закрепление, комментирование, реакции

Во всех apps.py добавлен импорт rules для автозагрузки правил при старте Django
2026-02-28 00:13:46 +03:00

377 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
django-rules: декларативные правила доступа для 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)
"""