""" Реализация RBAC операций для использования через интерфейс. Этот модуль предоставляет конкретную реализацию RBAC операций, не импортирует ORM модели напрямую, используя dependency injection. """ import asyncio import json from pathlib import Path from typing import Any from auth.orm import Author from auth.rbac_interface import CommunityAuthorQueries, RBACOperations, get_community_queries from services.db import local_session from services.redis import redis from settings import ADMIN_EMAILS from utils.logger import root_logger as logger # --- Загрузка каталога сущностей и дефолтных прав --- with Path("services/permissions_catalog.json").open() as f: PERMISSIONS_CATALOG = json.load(f) with Path("services/default_role_permissions.json").open() as f: DEFAULT_ROLE_PERMISSIONS = json.load(f) role_names = list(DEFAULT_ROLE_PERMISSIONS.keys()) class RBACOperationsImpl(RBACOperations): """Конкретная реализация RBAC операций""" async def get_permissions_for_role(self, role: str, community_id: int) -> list[str]: """ Получает список разрешений для конкретной роли в сообществе. Иерархия уже применена при инициализации сообщества. Args: role: Название роли community_id: ID сообщества Returns: Список разрешений для роли """ role_perms = await self._get_role_permissions_for_community(community_id) return role_perms.get(role, []) async def initialize_community_permissions(self, community_id: int) -> None: """ Инициализирует права для нового сообщества на основе дефолтных настроек с учетом иерархии. Args: community_id: ID сообщества """ key = f"community:roles:{community_id}" # Проверяем, не инициализировано ли уже existing = await redis.execute("GET", key) if existing: logger.debug(f"Права для сообщества {community_id} уже инициализированы") return # Создаем полные списки разрешений с учетом иерархии expanded_permissions = {} def get_role_permissions(role: str, processed_roles: set[str] | None = None) -> set[str]: """ Рекурсивно получает все разрешения для роли, включая наследованные Args: role: Название роли processed_roles: Список уже обработанных ролей для предотвращения зацикливания Returns: Множество разрешений """ if processed_roles is None: processed_roles = set() if role in processed_roles: return set() processed_roles.add(role) # Получаем прямые разрешения роли direct_permissions = set(DEFAULT_ROLE_PERMISSIONS.get(role, [])) # Проверяем, есть ли наследование роли for perm in list(direct_permissions): if perm in role_names: # Если пермишен - это название роли, добавляем все её разрешения direct_permissions.remove(perm) direct_permissions.update(get_role_permissions(perm, processed_roles)) return direct_permissions # Формируем расширенные разрешения для каждой роли for role in role_names: expanded_permissions[role] = list(get_role_permissions(role)) # Сохраняем в Redis уже развернутые списки с учетом иерархии await redis.execute("SET", key, json.dumps(expanded_permissions)) logger.info(f"Инициализированы права с иерархией для сообщества {community_id}") async def user_has_permission( self, author_id: int, permission: str, community_id: int, session: Any = None ) -> bool: """ Проверяет, есть ли у пользователя конкретное разрешение в сообществе. Args: author_id: ID автора permission: Разрешение для проверки community_id: ID сообщества session: Опциональная сессия БД (для тестов) Returns: True если разрешение есть, False если нет """ community_queries = get_community_queries() user_roles = community_queries.get_user_roles_in_community(author_id, community_id, session) return await self._roles_have_permission(user_roles, permission, community_id) async def _get_role_permissions_for_community(self, community_id: int) -> dict: """ Получает права ролей для конкретного сообщества. Если права не настроены, автоматически инициализирует их дефолтными. Args: community_id: ID сообщества Returns: Словарь прав ролей для сообщества """ key = f"community:roles:{community_id}" data = await redis.execute("GET", key) if data: return json.loads(data) # Автоматически инициализируем, если не найдено await self.initialize_community_permissions(community_id) # Получаем инициализированные разрешения data = await redis.execute("GET", key) if data: return json.loads(data) # Fallback на дефолтные разрешения если что-то пошло не так return DEFAULT_ROLE_PERMISSIONS async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool: """ Проверяет, есть ли у набора ролей конкретное разрешение в сообществе. Args: role_slugs: Список ролей для проверки permission: Разрешение для проверки community_id: ID сообщества Returns: True если хотя бы одна роль имеет разрешение """ role_perms = await self._get_role_permissions_for_community(community_id) return any(permission in role_perms.get(role, []) for role in role_slugs) class CommunityAuthorQueriesImpl(CommunityAuthorQueries): """Конкретная реализация запросов CommunityAuthor через поздний импорт""" def get_user_roles_in_community( self, author_id: int, community_id: int = 1, session: Any = None ) -> list[str]: """ Получает роли пользователя в сообществе через новую систему CommunityAuthor """ # Поздний импорт для избежания циклических зависимостей from orm.community import CommunityAuthor try: if session: ca = ( session.query(CommunityAuthor) .where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id) .first() ) return ca.role_list if ca else [] # Используем local_session для продакшена with local_session() as db_session: ca = ( db_session.query(CommunityAuthor) .where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id) .first() ) return ca.role_list if ca else [] except Exception as e: logger.error(f"[get_user_roles_in_community] Ошибка при получении ролей: {e}") return [] # Создаем экземпляры реализаций rbac_operations = RBACOperationsImpl() community_queries = CommunityAuthorQueriesImpl()