### 🔄 Изменения - **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации - **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)` ### 🧪 Тестирование - **Исправление тестов** - адаптация к новой структуре моделей - **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py` - **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев - **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями - **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода ### 🔧 Рефакторинг - **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру - **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль - **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры - **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей - **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки ### 🔧 Авторизация с cookies - **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization - **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно - **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token` - **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession` - **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author` - **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами - **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession` - **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации ### 📝 Документация - **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа - Обновлена документация RBAC - Обновлена документация авторизации с cookies
This commit is contained in:
20
rbac/api.py
20
rbac/api.py
@@ -12,10 +12,10 @@ import asyncio
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.author import Author
|
||||
from rbac.interface import get_community_queries, get_rbac_operations
|
||||
from storage.db import local_session
|
||||
from settings import ADMIN_EMAILS
|
||||
from storage.db import local_session
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@@ -46,6 +46,20 @@ async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
|
||||
return await rbac_ops.get_permissions_for_role(role, community_id)
|
||||
|
||||
|
||||
async def get_role_permissions_for_community(community_id: int) -> dict:
|
||||
"""
|
||||
Получает все разрешения для всех ролей в сообществе.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Словарь {роль: [разрешения]} для всех ролей
|
||||
"""
|
||||
rbac_ops = get_rbac_operations()
|
||||
return await rbac_ops.get_all_permissions_for_community(community_id)
|
||||
|
||||
|
||||
async def update_all_communities_permissions() -> None:
|
||||
"""
|
||||
Обновляет права для всех существующих сообществ на основе актуальных дефолтных настроек.
|
||||
@@ -121,7 +135,7 @@ async def roles_have_permission(role_slugs: list[str], permission: str, communit
|
||||
True если хотя бы одна роль имеет разрешение
|
||||
"""
|
||||
rbac_ops = get_rbac_operations()
|
||||
return await rbac_ops._roles_have_permission(role_slugs, permission, community_id)
|
||||
return await rbac_ops.roles_have_permission(role_slugs, permission, community_id)
|
||||
|
||||
|
||||
# --- Декораторы ---
|
||||
|
||||
@@ -28,7 +28,15 @@ class RBACOperations(Protocol):
|
||||
"""Проверяет разрешение пользователя в сообществе"""
|
||||
...
|
||||
|
||||
async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
async def get_role_permissions_for_community(self, community_id: int, role: str) -> dict:
|
||||
"""Получает права для конкретной роли в сообществе"""
|
||||
...
|
||||
|
||||
async def get_all_permissions_for_community(self, community_id: int) -> dict:
|
||||
"""Получает все права ролей для конкретного сообщества"""
|
||||
...
|
||||
|
||||
async def roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
"""Проверяет, есть ли у набора ролей конкретное разрешение в сообществе"""
|
||||
...
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class RBACOperationsImpl(RBACOperations):
|
||||
Returns:
|
||||
Список разрешений для роли
|
||||
"""
|
||||
role_perms = await self._get_role_permissions_for_community(community_id)
|
||||
role_perms = await self.get_role_permissions_for_community(community_id, role)
|
||||
return role_perms.get(role, [])
|
||||
|
||||
async def initialize_community_permissions(self, community_id: int) -> None:
|
||||
@@ -117,18 +117,52 @@ class RBACOperationsImpl(RBACOperations):
|
||||
"""
|
||||
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)
|
||||
return await self.roles_have_permission(user_roles, permission, community_id)
|
||||
|
||||
async def _get_role_permissions_for_community(self, community_id: int) -> dict:
|
||||
async def get_role_permissions_for_community(self, community_id: int, role: str) -> dict:
|
||||
"""
|
||||
Получает права ролей для конкретного сообщества.
|
||||
Получает права для конкретной роли в сообществе, включая все наследованные разрешения.
|
||||
Если права не настроены, автоматически инициализирует их дефолтными.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
role: Название роли для получения разрешений
|
||||
|
||||
Returns:
|
||||
Словарь {роль: [разрешения]} для указанной роли с учетом наследования
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
data = await redis.execute("GET", key)
|
||||
|
||||
if data:
|
||||
role_permissions = json.loads(data)
|
||||
if role in role_permissions:
|
||||
return {role: role_permissions[role]}
|
||||
# Если роль не найдена в кеше, используем рекурсивный расчет
|
||||
|
||||
# Автоматически инициализируем, если не найдено
|
||||
await self.initialize_community_permissions(community_id)
|
||||
|
||||
# Получаем инициализированные разрешения
|
||||
data = await redis.execute("GET", key)
|
||||
if data:
|
||||
role_permissions = json.loads(data)
|
||||
if role in role_permissions:
|
||||
return {role: role_permissions[role]}
|
||||
|
||||
# Fallback: рекурсивно вычисляем разрешения для роли
|
||||
return {role: list(self._get_role_permissions_recursive(role))}
|
||||
|
||||
async def get_all_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)
|
||||
@@ -147,7 +181,41 @@ class RBACOperationsImpl(RBACOperations):
|
||||
# Fallback на дефолтные разрешения если что-то пошло не так
|
||||
return DEFAULT_ROLE_PERMISSIONS
|
||||
|
||||
async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
def _get_role_permissions_recursive(self, role: str, processed_roles: set[str] | None = None) -> set[str]:
|
||||
"""
|
||||
Рекурсивно получает все разрешения для роли, включая наследованные.
|
||||
Вспомогательный метод для вычисления разрешений без обращения к Redis.
|
||||
|
||||
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, []))
|
||||
|
||||
# Проверяем, есть ли наследование роли
|
||||
inherited_permissions = set()
|
||||
for perm in list(direct_permissions):
|
||||
if perm in role_names:
|
||||
# Если пермишен - это название роли, добавляем все её разрешения
|
||||
direct_permissions.remove(perm)
|
||||
inherited_permissions.update(self._get_role_permissions_recursive(perm, processed_roles))
|
||||
|
||||
# Объединяем прямые и наследованные разрешения
|
||||
return direct_permissions | inherited_permissions
|
||||
|
||||
async def roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у набора ролей конкретное разрешение в сообществе.
|
||||
|
||||
@@ -159,8 +227,12 @@ class RBACOperationsImpl(RBACOperations):
|
||||
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)
|
||||
# Получаем разрешения для каждой роли с учетом наследования
|
||||
for role in role_slugs:
|
||||
role_perms = await self.get_role_permissions_for_community(community_id, role)
|
||||
if permission in role_perms.get(role, []):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CommunityAuthorQueriesImpl(CommunityAuthorQueries):
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.author import Author
|
||||
from orm.community import Community, CommunityAuthor
|
||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||
|
||||
|
||||
Reference in New Issue
Block a user