lizerk 80cdc36d92 fix: исправлена система прав доступа к чатам и сообщениям
Backend:
- Исправлен user_can_access_chat() для поддержки context-based чатов
- Обновлен is_chat_member() для использования get_participants()
- Добавлена проверка has_send_messages_permission (блокирует гостей)
- Исправлен MessageViewSet.get_queryset() для комментариев
- Добавлена поддержка Feed Post resolver в resolve_chat_participants()
- Чаты типа 'comments' исключены из общего списка
- Добавлены permissions для mark_read, messages, messages_around
- Добавлена команда manage_chat_permissions для управления ролями

Frontend:
- Скрыто поле ввода сообщений для гостей (role='guest')
- Добавлена проверка can_send_messages из membership
- Показывается информативная заглушка для пользователей без прав

Tests & Docs:
- Добавлены тесты для guest роли (send_message + reactions)
- Документация по ролям и правам в чатах
- Troubleshooting guide для отладки permissions

Fixes:
- 403 при доступе к чатам-комментариям
- 404 при удалении/редактировании комментариев
- Комментарии в общем списке чатов
- Гости могли отправлять сообщения
2026-03-12 14:30:25 +03:00

152 lines
5.5 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.

"""
Утилиты для модуля communications.
Переиспользуемые функции для API, WebSocket и других компонентов.
"""
from __future__ import annotations
import datetime
from datetime import datetime as dt
from datetime import timezone as dt_tz
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from .models import Chat, ChatMembership
def _coerce_ts(val: str | None) -> datetime.datetime:
"""
Принимает миллисекунды/секунды с эпохи или ISO-дату.
Возвращает aware-дату (UTC). Фоллбек — timezone.now().
Args:
val: Строка с timestamp (ms/sec) или ISO-дата
Returns:
datetime: Aware datetime объект в UTC
Examples:
>>> _coerce_ts("1640000000000") # миллисекунды
datetime.datetime(2021, 12, 20, 13, 33, 20, tzinfo=UTC)
>>> _coerce_ts("1640000000") # секунды
datetime.datetime(2021, 12, 20, 13, 33, 20, tzinfo=UTC)
>>> _coerce_ts("2021-12-20T13:33:20Z") # ISO
datetime.datetime(2021, 12, 20, 13, 33, 20, tzinfo=UTC)
"""
if not val:
return timezone.now()
# Сначала пробуем число (sec/ms)
try:
iv = int(val)
if iv > 10**12: # пришли миллисекунды
iv //= 1000
return dt.fromtimestamp(iv, tz=dt_tz.utc)
except Exception:
pass
# Потом пробуем ISO-строку
d = parse_datetime(val)
if d is None:
return timezone.now()
if timezone.is_naive(d):
return timezone.make_aware(d, timezone=timezone.get_current_timezone())
return d
def user_can_access_chat(chat: Chat, user) -> bool:
"""
Проверяет, имеет ли пользователь доступ к чату.
Правила доступа по типам чатов:
- global: доступ всем
- private: только участники (participants)
- group: участники (participants) или ChatMembership
- channel/announcement: include_all_users или participants/membership
- comments: context-based доступ через get_participants()
Args:
chat: Объект Chat
user: Объект User
Returns:
bool: True если пользователь имеет доступ
Note:
Использует прямые запросы к БД в обход prefetch cache
"""
if not chat or not user:
return False
if chat.type == "global":
return True
if chat.type == "private":
# Личные чаты только через participants
# Обходим prefetch cache используя прямой запрос
from .models import Chat
return Chat.objects.filter(pk=chat.pk, participants=user).exists()
if chat.type == "group":
# Групповые чаты с контекстным объектом используют resolver
if chat.context_object_id:
try:
participants = chat.get_participants()
return participants.filter(pk=user.pk).exists()
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"[user_can_access_chat] Failed to resolve for chat "
f"{chat.id}: {e}"
)
# Обычные групповые чаты через participants ИЛИ ChatMembership
from .models import Chat
in_participants = Chat.objects.filter(
pk=chat.pk, participants=user
).exists()
in_membership = ChatMembership.objects.filter(
chat=chat, user=user
).exists()
return in_participants or in_membership
if chat.type in ("channel", "announcement"):
# Для каналов и объявлений может быть include_all_users или membership
if chat.include_all_users:
return user.is_active
# Проверяем и participants и membership для гибкости
from .models import Chat
return (
Chat.objects.filter(pk=chat.pk, participants=user).exists()
or ChatMembership.objects.filter(chat=chat, user=user).exists()
)
if chat.type == "comments":
# Чаты-комментарии ВСЕГДА используют context-based доступ
if chat.context_object_id:
try:
# get_participants() вызовет resolver который учитывает
# права доступа к контекстному объекту
participants = chat.get_participants()
return participants.filter(pk=user.pk).exists()
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"[user_can_access_chat] Failed to resolve for "
f"comments chat {chat.id}: {e}"
)
return False
# Fallback для старых чатов без контекста
from .models import Chat
return (
Chat.objects.filter(pk=chat.pk, participants=user).exists()
or ChatMembership.objects.filter(chat=chat, user=user).exists()
)
return False