[0.9.7] - 2025-08-18
Some checks failed
Deploy on push / deploy (push) Failing after 2m22s

### 🔄 Изменения
- **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:
2025-08-18 14:25:25 +03:00
parent 9a2b792f08
commit 1b48675b92
78 changed files with 1658 additions and 1050 deletions

View File

@@ -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)
# --- Декораторы ---

View File

@@ -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:
"""Проверяет, есть ли у набора ролей конкретное разрешение в сообществе"""
...

View File

@@ -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):

View File

@@ -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