370 lines
12 KiB
Python
370 lines
12 KiB
Python
"""
|
||
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, # Все авторизованные
|
||
)
|
||
|
||
|
||
# Примеры использования перенесены в проектную документацию.
|