Corp/backend/docs/completed/CHAT_PERMISSIONS_FIX.md
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

18 KiB
Raw Blame History

Исправление системы прав доступа в Communications

Дата: 12.03.2026
Статус: Завершено
Тесты: ✅ Все тесты прошли (6/6 send_message + 6/6 reactions)

Проблемы

1. Пользователь не может добавить участника в групповой чат

При попытке пользователя добавить участника в групповой чат возникала ошибка 403 Forbidden:

[WARNING] communications.rules:66 [is_chat_owner] checking via created_by: 
  user=10, created_by=1, result=False
[WARNING] django.request:253 Forbidden: /api/v1/communications/chats/62/
[WARNING] django.channels.server:191 HTTP POST /api/v1/communications/chats/62/add-member/ 403

Причины:

  • Неверный related_name в предикате is_chat_admin: использовался chatmembership_set вместо memberships
  • Отсутствие проверки персональных прав: флаги can_add_members, can_remove_members, can_pin_messages не использовались

2. Гость может отправлять сообщения (хотя не должен)

Пользователь с ролью guest (can_send_messages=False) мог отправлять сообщения в чат.

# Тест падал
def test_guest_cannot_send_messages(self, guest_client, group_chat_with_roles):
    """Гость НЕ может отправлять сообщения"""
    response = guest_client.post(url, {'chat': chat_id, 'content': 'Message'})
    assert response.status_code == status.HTTP_403_FORBIDDEN  # ❌ Получали 201

Причины:

  • Правило communications.send_message не проверяло флаг can_send_messages
  • MessagePermission.has_permission() не проверяло права при создании сообщения

3. Гость не может отправлять реакции (хотя должен)

Гость с ролью guest не мог ставить реакции (эмодзи) на сообщения, получал ошибку или отклонение.

Причины:

  • MessagePermission.has_object_permission() не обрабатывал custom actions (react, unreact)
  • POST запросы на /messages/{id}/react/ отклонялись как неразрешенные

Решение

1. Исправлен предикат is_chat_admin

Файл: backend/communications/rules.py

@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'):  # ← БЫЛО: chatmembership_set
        result = chat.memberships.filter(
            user=user,
            role__in=['admin', 'moderator'],
            is_active=True
        ).exists()
        logger.warning(
            f"[is_chat_admin] user={user.id}, chat={chat.id}, "
            f"result={result}"
        )
        return result
    
    logger.warning(
        f"[is_chat_admin] no memberships for chat {chat.id}"
    )
    return False

2. Добавлены новые предикаты для персональных прав

@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.warning(
            f"[can_add_members_flag] user={user.id}, "
            f"chat={chat.id}, result={result}"
        )
        return result

    return False


@rules.predicate
def can_remove_members_flag(user, chat):
    """Пользователь имеет флаг can_remove_members в ChatMembership"""
    # Аналогично can_add_members_flag
    

@rules.predicate
def can_pin_messages_flag(user, chat):
    """Пользователь имеет флаг can_pin_messages в ChatMembership"""
    # Аналогично can_add_members_flag

3. Добавлена проверка прав на отправку сообщений

Файл: backend/communications/rules.py

@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
    
    # Для чатов с ChatMembership проверяем флаг can_send_messages
    if hasattr(chat, 'memberships'):
### 5. Обновлены правила доступаrn membership.can_send_messages
        except ChatMembership.DoesNotExist:
            # Если нет membership, но пользователь в participants - разрешено
            return chat.participants.filter(pk=user.pk).exists()
    
    # Если нет memberships (старые чаты) - разрешено
    return True


# Обновленное правило
rules.add_rule(
    'communications.send_message',
    is_superuser | (is_chat_member & has_send_messages_permission)
    # ↑ добавлена проверка флага can_send_messages
)

4. Добавлена проверка в MessagePermission

Файл: backend/communications/api/permissions.py

class MessagePermission(permissions.BasePermission):
    """
    Проверка прав доступа к сообщениям через django-rules
    
    Правила:
    - GET - доступ к чату сообщения
    - POST создание - участник чата + can_send_messages
    - POST react/unreact - участник чата (включая гостей)  # ← НОВОЕ
    - PUT/PATCH - автор сообщения
    - DELETE - автор или админ чата
    """
    
    def has_permission(self, request, view):
        """Базовая проверка при создании сообщения"""
        if not (request.user and request.user.is_authenticated):
            return False
        
        # При создании сообщения проверяем права на отправку в чат
        if request.method == 'POST' and view.action in ['create', 'upload']:
            chat_id = (
                request.data.get('chat') or
                request.data.get('chat_id')
            )
            
            if chat_id:
                try:
                    from ..models import Chat
                    chat = Chat.objects.get(pk=chat_id)
                    
                    # ✅ Проверка через django-rules
                    return rules.test_rule(
                        'communications.send_message',
                        request.user,
                        chat
                    )
                except Chat.DoesNotExist:
                    return False
        
        return True
    
    def has_object_permission(self, request, view, obj):
        """Проверка прав на конкретное сообщение"""
        # ✅ НОВОЕ: Реакции доступны всем участникам чата (включая гостей)
        if hasattr(view, 'action') and view.action in ['react', 'unreact']:
            return rules.test_rule(
                'communications.view_message',
                request.user,
                obj
            )
        
        # Просмотр - доступ к чату
        if request.method in permissions.SAFE_METHODS:
            return rules.test_rule(
                'communications.view_message',
                request.user,
                obj
            )
        
        # Редактирование - автор
        if request.method in ['PUT', 'PATCH']:
            return rules.test_rule(
                'communications.change_message',
                request.user,
                obj
            )
        
        # Удаление - автор или админ
        if request.method == 'DELETE':
            return rules.test_rule(
                'communications.delete_message',
                request.user,
                obj
            )
        
        return False
# Добавление участников в чат
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
    # ↑ добавлен новый предикат
)

Иерархия прав

Теперь система поддерживает три уровня прав доступа:

1. 🔴 Владелец чата (Chat.created_by)

  • Полные права на управление чатом
  • Может изменять настройки, добавлять/удалять участников
  • Не может покинуть чат (владелец не может уйти)

2. 🟠 Администратор/Модератор (ChatMembership.role)

  • Роль admin: широкие права на управление чатом
  • Роль moderator: модерация контента, может удалять чужие сообщения
  • Назначается через поле role в таблице ChatMembership

3. 🟢 Персональные права (ChatMembership флаги)

  • can_add_members - может добавлять участников
  • can_remove_members - может удалять участников
  • can_pin_messages - может закреплять сообщения
  • can_send_messages - может отправлять сообщения

Как использовать

Назначить администратора через Django Admin or Shell

from communications.models import Chat, ChatMembership
from employees.models import Employee

chat = Chat.objects.get(pk=62)
user = Employee.objects.get(pk=10)

# Создать или обновить membership
membership, created = ChatMembership.objects.get_or_create(
    chat=chat,
    user=user,
    defaults={
        'role': 'admin',  # или 'moderator', 'member', 'guest'
        'can_add_members': True,
        'can_remove_members': True,
        'can_pin_messages': True,
        'invited_by': chat.created_by
    }
)

if not created:
    # Обновить существующий membership
    membership.role = 'admin'
    membership.can_add_members = True
    membership.can_remove_members = True
    membership.can_pin_messages = True
    membership.save()

print(f"✅ Пользователь {user.get_full_name()} назначен администратором чата")

Назначить персональные права (без роли admin)

membership, created = ChatMembership.objects.get_or_create(
    chat=chat,
    user=user,
    defaults={
        'role': 'member',  # обычный участник
**Тесты отправки сообщений:**

test_owner_can_send_messages PASSED ✅ test_admin_can_send_messages PASSED ✅ test_moderator_can_send_messages PASSED ✅ test_member_can_send_messages PASSED ✅ test_guest_cannot_send_messages PASSED ✅ (было: FAILED) test_non_member_cannot_send_messages PASSED ✅


**Тесты отправки реакций:**

test_owner_can_react PASSED ✅ test_admin_can_react PASSED ✅ test_moderator_can_react PASSED ✅ test_member_can_react PASSED ✅ test_guest_can_react PASSED ✅ (НОВЫЙ) test_non_member_cannot_react PASSED

import rules

# Проверка прав на добавление участников
can_add = rules.test_rule('communications.add_members', user, chat)
print(f"Can add members: {can_add}")

# Проверка прав на изменение чата
can_change = rules.test_rule('communications.change_chat', user, chat)
print(f"Can change chat: {can_change}")

API Endpoints

После исправления следующие endpoints доступны пользователям с правами:

  • POST /api/v1/communications/chats/{id}/add-member/ - добавить участника

    • Требует: is_superuser | is_chat_owner | is_chat_admin | can_add_members_flag
  • POST /api/v1/communications/chats/{id}/remove-member/ - удалить участника

    • Требует: is_superuser | is_chat_owner | is_chat_admin | can_remove_members_flag
  • PATCH /api/v1/communications/chats/{id}/ - изменить чат

    • Требует: is_superuser | is_chat_owner | is_chat_admin
  • POST /api/v1/communications/chats/{id}/pin/ - закрепить чат

    • Требует: is_superuser | is_chat_owner | is_chat_admin | can_pin_messages_flag

Тестирование

Запуск тестов

cd backend
.venv/bin/python -m pytest tests/api/v1/communications/test_chat_roles.py::TestSendMessagePermissions -v

Результаты (все пройдены ✅)

test_owner_can_send_messages PASSED              ✅
**Отправка сообщений:**
1. ✅ Владелец чата может отправлять сообщения
2. ✅ Администратор (role=admin) может отправлять сообщения
3. ✅ Модератор (role=moderator) может отправлять сообщения
4. ✅ Обычный участник (role=member) может отправлять сообщения
5. ✅ Гость (role=guest) НЕ может отправлять сообщения
6. ✅ Не-участник чата НЕ может отправлять сообщения

**Реакции (эмодзи):**
7. ✅ Владелец чата может ставить реакции
8. ✅ Администратор может ставить реакции
9. ✅ Модератор может ставить реакции
10. ✅ Обычный участник может ставить реакции
11. ✅ Гость МОЖЕТ ставить реакции (даже без can_send_messages)
12. ✅ Не-участник чата НЕ может ставить реакции

**Управление участниками:**
13. ✅ Владелец чата может добавлять участников  
14. ✅ Администратор (role=admin) может добавлять участников
15. ✅ Модератор (role=moderator) может добавлять участников
16. ✅ Обычный участник с can_add_members=True может добавлять участников
17
1. ✅ Владелец чата может отправлять сообщения
2. ✅ Администратор (role=admin) может отправлять сообщения
3. ✅ Модератор (role=moderator) может отправлять сообщения
4. ✅ Обычный участник (role=member) может отправлять сообщения
5. ✅ Гость (role=guest) НЕ может отправлять сообщения
6. ✅ Не-участник чата НЕ может отправлять сообщения
7. ✅ Владелец чата может добавлять участников  
8. ✅ Администратор (role=admin) может добавлять участников
9. ✅ Модератор (role=moderator) может добавлять участников
10. ✅ Обычный участник с can_add_members=True может добавлять участников
11. ❌ Обычный участник без прав НЕ может добавлять участников

## Логирование

Все проверки прав записываются в лог с уровнем WARNING:

```log
[can_add_members_flag] user=10, chat=62, result=True
[is_chat_admin] user=10, chat=62, result=True

Это помогает отладить проблемы с правами доступа.

Совместимость

✅ Изменения обратно совместимы:

  • Старые чаты без ChatMembership работают как раньше
  • Личные чаты (type=private) используют только поле participants
  • Глобальные чаты (type=global) доступны всем
  • Групповые чаты (type=group) используют ChatMembership

Примечания

  • Роль owner в ChatMembership НЕ используется - владелец определяется через Chat.created_by
  • Флаг is_active=False означает, что пользователь покинул чат
  • При добавлении участника автоматически создается запись в ChatMembership с role='member'