""" django-rules: декларативные правила доступа для communications (мессенджер) Правила используются для проверки permissions на уровне объектов. https://github.com/dfunckt/django-rules """ import logging import rules logger = logging.getLogger(__name__) # ----------------------------------------------------------------------------- # ПРЕДИКАТЫ (predicates) # ----------------------------------------------------------------------------- @rules.predicate def is_superuser(user): """Суперпользователь имеет все права""" return user.is_superuser @rules.predicate def is_chat_member(user, chat): """Пользователь является участником чата""" from .utils import user_can_access_chat return user_can_access_chat(chat, user) @rules.predicate def is_chat_owner(user, chat): """Пользователь является владельцем/создателем чата""" if chat is None: return False if hasattr(chat, "owner"): owner = chat.owner result = owner.id == user.id if owner else False return result if hasattr(chat, "created_by"): created_by = chat.created_by result = created_by.id == user.id if created_by else False return result logger.warning( "Chat ownership cannot be evaluated", extra={ "event": "chat.permission.schema_mismatch", "chat_id": getattr(chat, "id", None), "permission": "owner", }, ) 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, "memberships"): result = chat.memberships.filter( user=user, role__in=["admin", "moderator"], is_active=True ).exists() return result logger.warning( "Chat administration cannot be evaluated", extra={ "event": "chat.permission.schema_mismatch", "chat_id": chat.id, "permission": "admin", }, ) return False @rules.predicate def can_add_members_flag(user, chat): """Пользователь имеет флаг can_add_members в ChatMembership""" if chat is None: return False if hasattr(chat, "memberships"): result = chat.memberships.filter( user=user, can_add_members=True, is_active=True ).exists() logger.debug( "Chat permission evaluated", extra={ "event": "chat.permission.evaluated", "user_id": user.id, "chat_id": chat.id, "permission": "add_members", "allowed": result, }, ) return result return False @rules.predicate def can_remove_members_flag(user, chat): """Пользователь имеет флаг can_remove_members в ChatMembership""" if chat is None: return False if hasattr(chat, "memberships"): result = chat.memberships.filter( user=user, can_remove_members=True, is_active=True ).exists() logger.debug( "Chat permission evaluated", extra={ "event": "chat.permission.evaluated", "user_id": user.id, "chat_id": chat.id, "permission": "remove_members", "allowed": result, }, ) return result return False @rules.predicate def can_pin_messages_flag(user, chat): """Пользователь имеет флаг can_pin_messages в ChatMembership""" if chat is None: return False if hasattr(chat, "memberships"): result = chat.memberships.filter( user=user, can_pin_messages=True, is_active=True ).exists() logger.debug( "Chat permission evaluated", extra={ "event": "chat.permission.evaluated", "user_id": user.id, "chat_id": chat.id, "permission": "pin_messages", "allowed": result, }, ) return result return False @rules.predicate def has_send_messages_permission(user, chat): """ Пользователь имеет право отправлять сообщения в чат Проверяет: 1. Если НЕТ ChatMembership - разрешено (для обратной совместимости) 2. Если ЕСТЬ ChatMembership - проверяется флаг can_send_messages """ if chat is None: return False # Для личных/глобальных чатов без ChatMembership - разрешено всем if chat.type in ["private", "global"]: return True if chat.type == "comments" and chat.context_object_id: try: return chat.get_participants().filter(pk=user.pk).exists() except Exception: logger.exception( "Comments chat participants could not be resolved", extra={ "event": "chat.permission.participant_resolution_failed", "user_id": user.id, "chat_id": chat.id, "permission": "send_messages", }, ) return False # Для чатов с ChatMembership проверяем флаг can_send_messages if hasattr(chat, "memberships"): from .models import ChatMembership # Проверяем, есть ли membership для пользователя try: membership = chat.memberships.get(user=user, is_active=True) result = membership.can_send_messages logger.debug( "Chat permission evaluated", extra={ "event": "chat.permission.evaluated", "user_id": user.id, "chat_id": chat.id, "permission": "send_messages", "allowed": result, }, ) return result except ChatMembership.DoesNotExist: if chat.participants.filter(pk=user.pk).exists(): logger.warning( "Chat permission granted through legacy participant fallback", extra={ "event": "chat.permission.legacy_fallback_used", "user_id": user.id, "chat_id": chat.id, "permission": "send_messages", }, ) return True logger.warning( "Chat permission denied because membership is missing", extra={ "event": "chat.permission.denied", "user_id": user.id, "chat_id": chat.id, "permission": "send_messages", "reason": "membership_missing", }, ) return False # Если нет memberships (не должно быть после миграции) - запрещено 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 getattr(message, "sender", None) == 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 deprecated_is_department_chat(user, chat): """ DEPRECATED: This predicate is deprecated. Use callback resolver pattern via get_participants() instead. For backward compatibility, always returns False. Configure COMMUNICATIONS_PARTICIPANT_RESOLVER in settings.py. """ 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 & has_send_messages_permission), ) # Изменение чата (название, описание, настройки) 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 | is_chat_admin ) # Просмотр сообщения 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 | can_add_members_flag, ) # Удаление участников из чата rules.add_rule( "communications.remove_members", is_superuser | is_chat_owner | is_chat_admin | can_remove_members_flag, ) # Закрепление сообщений rules.add_rule( "communications.pin_message", is_superuser | is_chat_owner | is_chat_admin | can_pin_messages_flag, ) # Изменение ролей участников 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, # Все авторизованные ) # Примеры использования перенесены в проектную документацию.