2025-06-02 02:56:11 +03:00
|
|
|
|
from collections.abc import Callable
|
2025-05-16 09:23:48 +03:00
|
|
|
|
from functools import wraps
|
2025-08-17 16:33:54 +03:00
|
|
|
|
from typing import Any
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
from graphql import GraphQLError, GraphQLResolveInfo
|
|
|
|
|
|
from sqlalchemy import exc
|
|
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
# Импорт базовых функций из реструктурированных модулей
|
|
|
|
|
|
from auth.core import authenticate
|
2025-08-17 17:56:31 +03:00
|
|
|
|
from auth.credentials import AuthCredentials
|
|
|
|
|
|
from auth.exceptions import OperationNotAllowedError
|
|
|
|
|
|
from auth.utils import get_auth_token, get_safe_headers
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **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
2025-08-18 14:25:25 +03:00
|
|
|
|
from orm.author import Author
|
2025-07-02 22:30:21 +03:00
|
|
|
|
from orm.community import CommunityAuthor
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **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
2025-08-18 14:25:25 +03:00
|
|
|
|
from storage.db import local_session
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from utils.logger import root_logger as logger
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
|
|
|
|
|
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
2025-05-19 11:25:41 +03:00
|
|
|
|
"""
|
2025-05-20 00:00:24 +03:00
|
|
|
|
Проверяет валидность GraphQL контекста и проверяет авторизацию.
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-19 11:25:41 +03:00
|
|
|
|
Args:
|
|
|
|
|
|
info: GraphQL информация о контексте
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-19 11:25:41 +03:00
|
|
|
|
Raises:
|
2025-05-20 00:00:24 +03:00
|
|
|
|
GraphQLError: если контекст невалиден или пользователь не авторизован
|
2025-05-19 11:25:41 +03:00
|
|
|
|
"""
|
2025-06-30 23:10:48 +03:00
|
|
|
|
# Подробное логирование для диагностики
|
|
|
|
|
|
logger.debug("[validate_graphql_context] Начало проверки контекста и авторизации")
|
|
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
# Проверка базовой структуры контекста
|
2025-05-19 11:25:41 +03:00
|
|
|
|
if info is None or not hasattr(info, "context"):
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning("[validate_graphql_context] Missing GraphQL context information")
|
2025-06-02 02:56:11 +03:00
|
|
|
|
msg = "Internal server error: missing context"
|
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-19 11:25:41 +03:00
|
|
|
|
|
|
|
|
|
|
request = info.context.get("request")
|
|
|
|
|
|
if not request:
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.error("[validate_graphql_context] Missing request in context")
|
2025-06-02 02:56:11 +03:00
|
|
|
|
msg = "Internal server error: missing request"
|
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-19 11:25:41 +03:00
|
|
|
|
|
2025-06-30 23:10:48 +03:00
|
|
|
|
# Логируем детали запроса
|
|
|
|
|
|
client_info = {
|
|
|
|
|
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
|
|
|
|
|
"headers_keys": list(get_safe_headers(request).keys()),
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.debug(f"[validate_graphql_context] Детали запроса: {client_info}")
|
|
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
# Проверяем auth из контекста - если уже авторизован, просто возвращаем
|
2025-05-19 11:25:41 +03:00
|
|
|
|
auth = getattr(request, "auth", None)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
if auth and getattr(auth, "logged_in", False):
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.debug(f"[validate_graphql_context] Пользователь уже авторизован через request.auth: {auth.author_id}")
|
2025-05-20 00:00:24 +03:00
|
|
|
|
return
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
# Если аутентификации нет в request.auth, пробуем получить ее из scope
|
2025-08-17 16:33:54 +03:00
|
|
|
|
token: str | None = None
|
2025-05-20 00:00:24 +03:00
|
|
|
|
if hasattr(request, "scope") and "auth" in request.scope:
|
|
|
|
|
|
auth_cred = request.scope.get("auth")
|
2025-07-02 22:30:21 +03:00
|
|
|
|
if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False):
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.debug(f"[validate_graphql_context] Пользователь авторизован через scope: {auth_cred.author_id}")
|
2025-05-21 18:29:32 +03:00
|
|
|
|
return
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-08-12 18:23:53 +03:00
|
|
|
|
# Если авторизации нет ни в auth, ни в scope, пробуем получить и проверить токен
|
|
|
|
|
|
token = await get_auth_token(request)
|
2025-05-20 00:00:24 +03:00
|
|
|
|
if not token:
|
2025-07-25 10:10:36 +03:00
|
|
|
|
# Если токен не найден, логируем как предупреждение, но не бросаем GraphQLError
|
2025-05-20 00:00:24 +03:00
|
|
|
|
client_info = {
|
|
|
|
|
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
2025-07-02 22:30:21 +03:00
|
|
|
|
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
|
2025-05-20 00:00:24 +03:00
|
|
|
|
}
|
2025-07-25 10:10:36 +03:00
|
|
|
|
logger.info(f"[validate_graphql_context] Токен авторизации не найден: {client_info}")
|
|
|
|
|
|
|
|
|
|
|
|
# Устанавливаем пустые учетные данные вместо выброса исключения
|
|
|
|
|
|
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
|
|
|
|
|
request.scope["auth"] = AuthCredentials(
|
|
|
|
|
|
author_id=None,
|
|
|
|
|
|
scopes={},
|
|
|
|
|
|
logged_in=False,
|
|
|
|
|
|
error_message="No authentication token",
|
|
|
|
|
|
email=None,
|
|
|
|
|
|
token=None,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-30 23:10:48 +03:00
|
|
|
|
# Логируем информацию о найденном токене
|
2025-08-12 18:23:53 +03:00
|
|
|
|
token_len = len(token) if hasattr(token, "__len__") else 0
|
|
|
|
|
|
logger.debug(f"[validate_graphql_context] Токен найден, длина: {token_len}")
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
# Используем единый механизм проверки токена из auth.internal
|
|
|
|
|
|
auth_state = await authenticate(request)
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"[validate_graphql_context] Результат аутентификации: logged_in={auth_state.logged_in}, author_id={auth_state.author_id}, error={auth_state.error}"
|
|
|
|
|
|
)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
if not auth_state.logged_in:
|
|
|
|
|
|
error_msg = auth_state.error or "Invalid or expired token"
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.warning(f"[validate_graphql_context] Недействительный токен: {error_msg}")
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = f"UnauthorizedError - {error_msg}"
|
2025-06-02 02:56:11 +03:00
|
|
|
|
raise GraphQLError(msg)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-16 20:20:23 +03:00
|
|
|
|
# Если все проверки пройдены, создаем AuthCredentials и устанавливаем в request.scope
|
2025-05-21 18:29:32 +03:00
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
try:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
author = session.query(Author).where(Author.id == auth_state.author_id).one()
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.debug(f"[validate_graphql_context] Найден автор: id={author.id}, email={author.email}")
|
|
|
|
|
|
|
2025-07-02 22:49:20 +03:00
|
|
|
|
# Создаем объект авторизации с пустыми разрешениями
|
|
|
|
|
|
# Разрешения будут проверяться через RBAC систему по требованию
|
2025-05-21 18:29:32 +03:00
|
|
|
|
auth_cred = AuthCredentials(
|
2025-06-02 02:56:11 +03:00
|
|
|
|
author_id=author.id,
|
2025-07-02 22:49:20 +03:00
|
|
|
|
scopes={}, # Пустой словарь разрешений
|
2025-06-02 02:56:11 +03:00
|
|
|
|
logged_in=True,
|
|
|
|
|
|
error_message="",
|
|
|
|
|
|
email=author.email,
|
|
|
|
|
|
token=auth_state.token,
|
2025-05-21 18:29:32 +03:00
|
|
|
|
)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-06-16 20:20:23 +03:00
|
|
|
|
# Устанавливаем auth в request.scope вместо прямого присваивания к request.auth
|
|
|
|
|
|
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
|
|
|
|
|
request.scope["auth"] = auth_cred
|
|
|
|
|
|
logger.debug(
|
2025-06-30 23:10:48 +03:00
|
|
|
|
f"[validate_graphql_context] Токен успешно проверен и установлен для пользователя {auth_state.author_id}"
|
2025-06-16 20:20:23 +03:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning("[validate_graphql_context] Не удалось установить auth: отсутствует request.scope")
|
2025-07-02 22:30:21 +03:00
|
|
|
|
msg = "Internal server error: unable to set authentication context"
|
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
except exc.NoResultFound:
|
2025-08-27 21:48:58 +03:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"[validate_graphql_context] Пользователь с ID {auth_state.author_id} не найден в базе данных"
|
|
|
|
|
|
)
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = "UnauthorizedError - user not found"
|
2025-06-16 20:20:23 +03:00
|
|
|
|
raise GraphQLError(msg) from None
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
return
|
2025-05-19 11:25:41 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
def admin_auth_required(resolver: Callable) -> Callable:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Декоратор для защиты админских эндпоинтов.
|
|
|
|
|
|
Проверяет принадлежность к списку разрешенных email-адресов.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
resolver: GraphQL резолвер для защиты
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-05-21 18:29:32 +03:00
|
|
|
|
Обернутый резолвер, который проверяет права доступа администратора
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
GraphQLError: если пользователь не авторизован или не имеет доступа администратора
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-19 11:25:41 +03:00
|
|
|
|
Example:
|
|
|
|
|
|
>>> @admin_auth_required
|
|
|
|
|
|
... async def admin_resolver(root, info, **kwargs):
|
|
|
|
|
|
... return "Admin data"
|
2025-05-16 09:23:48 +03:00
|
|
|
|
"""
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
@wraps(resolver)
|
2025-08-17 16:33:54 +03:00
|
|
|
|
async def wrapper(root: Any = None, info: GraphQLResolveInfo | None = None, **kwargs: dict[str, Any]) -> Any:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Подробное логирование для диагностики
|
|
|
|
|
|
logger.debug(f"[admin_auth_required] Начало проверки авторизации для {resolver.__name__}")
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Проверяем авторизацию пользователя
|
|
|
|
|
|
if info is None:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning("[admin_auth_required] GraphQL info is None")
|
2025-07-02 22:30:21 +03:00
|
|
|
|
msg = "Invalid GraphQL context"
|
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Логируем детали запроса
|
|
|
|
|
|
request = info.context.get("request")
|
|
|
|
|
|
client_info = {
|
|
|
|
|
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
|
|
|
|
|
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.debug(f"[admin_auth_required] Детали запроса: {client_info}")
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Проверяем наличие токена до validate_graphql_context
|
e2e-fixing
fix: убран health endpoint, E2E тест использует корневой маршрут
- Убран health endpoint из main.py (не нужен)
- E2E тест теперь проверяет корневой маршрут / вместо /health
- Корневой маршрут доступен без логина, что подходит для проверки состояния сервера
- E2E тест с браузером работает корректно
docs: обновлен отчет о прогрессе E2E теста
- Убраны упоминания health endpoint
- Указано что используется корневой маршрут для проверки серверов
- Обновлен список измененных файлов
fix: исправлены GraphQL проблемы и E2E тест с браузером
- Добавлено поле success в тип CommonResult для совместимости с фронтендом
- Обновлены резолверы community, collection, topic для возврата поля success
- Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint
- E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице
- Все GraphQL проблемы с полем success решены
- E2E тест работает правильно с браузером как требовалось
fix: исправлен поиск UI элементов в E2E тесте
- Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300
- Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×)
- Добавлен правильный поиск модального окна с множественными селекторами
- Добавлен правильный поиск кнопки подтверждения в модальном окне
- E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Обновлен отчет о прогрессе с полными результатами тестирования
fix: исправлен импорт require_any_permission в resolvers/collection.py
- Заменен импорт require_any_permission с auth.decorators на services.rbac
- Бэкенд сервер теперь запускается корректно
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Оба сервера (бэкенд и фронтенд) работают стабильно
fix: исправлен порядок импортов в resolvers/collection.py
- Перемещен импорт require_any_permission в правильное место
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности
feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
|
|
|
|
token = await get_auth_token(request)
|
2025-07-02 22:30:21 +03:00
|
|
|
|
logger.debug(f"[admin_auth_required] Токен найден: {bool(token)}, длина: {len(token) if token else 0}")
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
try:
|
|
|
|
|
|
# Проверяем авторизацию - НЕ ловим GraphQLError здесь!
|
2025-06-02 02:56:11 +03:00
|
|
|
|
await validate_graphql_context(info)
|
2025-06-30 23:10:48 +03:00
|
|
|
|
logger.debug("[admin_auth_required] validate_graphql_context успешно пройден")
|
2025-07-02 22:30:21 +03:00
|
|
|
|
except GraphQLError:
|
|
|
|
|
|
# Пробрасываем GraphQLError дальше - это ошибки авторизации
|
|
|
|
|
|
logger.debug("[admin_auth_required] GraphQLError от validate_graphql_context - пробрасываем дальше")
|
|
|
|
|
|
raise
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Получаем объект авторизации
|
|
|
|
|
|
auth = None
|
|
|
|
|
|
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
|
|
|
|
|
|
auth = info.context["request"].scope.get("auth")
|
|
|
|
|
|
logger.debug(f"[admin_auth_required] Auth из scope: {auth.author_id if auth else None}")
|
|
|
|
|
|
elif hasattr(info.context["request"], "auth"):
|
|
|
|
|
|
auth = info.context["request"].auth
|
|
|
|
|
|
logger.debug(f"[admin_auth_required] Auth из request: {auth.author_id if auth else None}")
|
|
|
|
|
|
else:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning("[admin_auth_required] Auth не найден ни в scope, ни в request")
|
2025-06-30 23:10:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
if not auth or not getattr(auth, "logged_in", False):
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = "UnauthorizedError - please login"
|
2025-07-02 22:30:21 +03:00
|
|
|
|
raise GraphQLError(msg)
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем, является ли пользователь администратором
|
|
|
|
|
|
try:
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
# Преобразуем author_id в int для совместимости с базой данных
|
|
|
|
|
|
author_id = int(auth.author_id) if auth and auth.author_id else None
|
|
|
|
|
|
if not author_id:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning(f"[admin_auth_required] ID автора не определен: {auth}")
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = "UnauthorizedError - invalid user ID"
|
2025-06-02 02:56:11 +03:00
|
|
|
|
raise GraphQLError(msg)
|
|
|
|
|
|
|
2025-07-31 18:55:59 +03:00
|
|
|
|
author = session.query(Author).where(Author.id == author_id).one()
|
2025-07-02 22:30:21 +03:00
|
|
|
|
logger.debug(f"[admin_auth_required] Найден автор: {author.id}, {author.email}")
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем, является ли пользователь системным администратором
|
|
|
|
|
|
if author.email and author.email in ADMIN_EMAILS:
|
|
|
|
|
|
logger.info(f"System admin access granted for {author.email} (ID: {author.id})")
|
|
|
|
|
|
return await resolver(root, info, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Системный администратор определяется ТОЛЬКО по ADMIN_EMAILS
|
|
|
|
|
|
logger.warning(f"System admin access denied for {author.email} (ID: {author.id}). Not in ADMIN_EMAILS.")
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = "UnauthorizedError - system admin access required"
|
2025-07-02 22:30:21 +03:00
|
|
|
|
raise GraphQLError(msg)
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
except exc.NoResultFound:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
2025-07-31 18:55:59 +03:00
|
|
|
|
msg = "UnauthorizedError - user not found"
|
2025-07-02 22:30:21 +03:00
|
|
|
|
raise GraphQLError(msg) from None
|
|
|
|
|
|
except GraphQLError:
|
|
|
|
|
|
# Пробрасываем GraphQLError дальше
|
|
|
|
|
|
raise
|
2025-05-16 09:23:48 +03:00
|
|
|
|
except Exception as e:
|
2025-07-02 22:30:21 +03:00
|
|
|
|
# Ловим только неожиданные ошибки, не GraphQLError
|
|
|
|
|
|
error_msg = f"Admin access error: {e!s}"
|
|
|
|
|
|
logger.error(f"[admin_auth_required] Неожиданная ошибка: {error_msg}")
|
2025-06-16 20:20:23 +03:00
|
|
|
|
raise GraphQLError(error_msg) from e
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
def permission_required(resource: str, operation: str, func: Callable) -> Callable:
|
2025-05-16 09:23:48 +03:00
|
|
|
|
"""
|
2025-05-20 00:00:24 +03:00
|
|
|
|
Декоратор для проверки разрешений.
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
resource: Ресурс для проверки
|
|
|
|
|
|
operation: Операция для проверки
|
2025-05-20 00:00:24 +03:00
|
|
|
|
func: Декорируемая функция
|
|
|
|
|
|
"""
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
@wraps(func)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def wrap(parent: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any) -> Any:
|
2025-05-21 18:29:32 +03:00
|
|
|
|
# Сначала проверяем авторизацию
|
|
|
|
|
|
await validate_graphql_context(info)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-21 18:29:32 +03:00
|
|
|
|
# Получаем объект авторизации
|
|
|
|
|
|
logger.debug(f"[permission_required] Контекст: {info.context}")
|
2025-06-16 20:20:23 +03:00
|
|
|
|
auth = None
|
|
|
|
|
|
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
|
|
|
|
|
|
auth = info.context["request"].scope.get("auth")
|
|
|
|
|
|
if not auth or not getattr(auth, "logged_in", False):
|
2025-06-02 02:56:11 +03:00
|
|
|
|
logger.error("[permission_required] Пользователь не авторизован после validate_graphql_context")
|
|
|
|
|
|
msg = "Требуются права доступа"
|
2025-07-31 18:55:59 +03:00
|
|
|
|
raise OperationNotAllowedError(msg)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
|
|
|
|
|
|
# Проверяем разрешения
|
2025-05-20 00:00:24 +03:00
|
|
|
|
with local_session() as session:
|
2025-05-21 18:29:32 +03:00
|
|
|
|
try:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
author = session.query(Author).where(Author.id == auth.author_id).one()
|
2025-05-20 00:00:24 +03:00
|
|
|
|
|
2025-05-21 18:29:32 +03:00
|
|
|
|
# Проверяем базовые условия
|
|
|
|
|
|
if author.is_locked():
|
2025-06-02 02:56:11 +03:00
|
|
|
|
msg = "Account is locked"
|
2025-07-31 18:55:59 +03:00
|
|
|
|
raise OperationNotAllowedError(msg)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
|
|
|
|
|
|
# Проверяем, является ли пользователь администратором (у них есть все разрешения)
|
|
|
|
|
|
if author.email in ADMIN_EMAILS:
|
|
|
|
|
|
logger.debug(f"[permission_required] Администратор {author.email} имеет все разрешения")
|
|
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-21 18:29:32 +03:00
|
|
|
|
# Проверяем роли пользователя
|
2025-05-29 12:37:39 +03:00
|
|
|
|
admin_roles = ["admin", "super"]
|
2025-07-02 22:30:21 +03:00
|
|
|
|
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
|
2025-07-31 18:55:59 +03:00
|
|
|
|
user_roles = ca.role_list if ca else []
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-21 18:29:32 +03:00
|
|
|
|
if any(role in admin_roles for role in user_roles):
|
2025-05-29 12:37:39 +03:00
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"[permission_required] Пользователь с ролью администратора {author.email} имеет все разрешения"
|
|
|
|
|
|
)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем разрешение
|
2025-07-31 18:55:59 +03:00
|
|
|
|
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
|
|
|
|
|
|
if ca:
|
|
|
|
|
|
user_roles = ca.role_list
|
|
|
|
|
|
if any(role in admin_roles for role in user_roles):
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"[permission_required] Пользователь с ролью администратора {author.email} имеет все разрешения"
|
|
|
|
|
|
)
|
|
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
2025-08-17 16:33:54 +03:00
|
|
|
|
if not ca or not ca.has_permission(f"{resource}:{operation}"):
|
2025-05-29 12:37:39 +03:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"[permission_required] У пользователя {author.email} нет разрешения {operation} на {resource}"
|
|
|
|
|
|
)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
msg = f"No permission for {operation} on {resource}"
|
2025-07-31 18:55:59 +03:00
|
|
|
|
raise OperationNotAllowedError(msg)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"[permission_required] Пользователь {author.email} имеет разрешение {operation} на {resource}"
|
|
|
|
|
|
)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
|
|
|
|
|
except exc.NoResultFound:
|
2025-08-27 18:31:51 +03:00
|
|
|
|
logger.warning(f"[permission_required] Пользователь с ID {auth.author_id} не найден в базе данных")
|
2025-06-02 02:56:11 +03:00
|
|
|
|
msg = "User not found"
|
2025-07-31 18:55:59 +03:00
|
|
|
|
raise OperationNotAllowedError(msg) from None
|
2025-05-20 00:00:24 +03:00
|
|
|
|
|
|
|
|
|
|
return wrap
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
def login_accepted(func: Callable) -> Callable:
|
2025-05-21 18:29:32 +03:00
|
|
|
|
"""
|
2025-06-30 21:25:26 +03:00
|
|
|
|
Декоратор для проверки аутентификации пользователя.
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-21 18:29:32 +03:00
|
|
|
|
Args:
|
2025-06-30 21:25:26 +03:00
|
|
|
|
func: функция-резолвер для декорирования
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Callable: обернутая функция
|
2025-05-21 18:29:32 +03:00
|
|
|
|
"""
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-20 00:00:24 +03:00
|
|
|
|
@wraps(func)
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def wrap(parent: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any) -> Any:
|
2025-05-21 18:29:32 +03:00
|
|
|
|
try:
|
2025-06-30 21:25:26 +03:00
|
|
|
|
await validate_graphql_context(info)
|
2025-05-21 18:29:32 +03:00
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
2025-06-30 21:25:26 +03:00
|
|
|
|
except GraphQLError:
|
|
|
|
|
|
# Пробрасываем ошибки авторизации далее
|
|
|
|
|
|
raise
|
2025-05-21 18:29:32 +03:00
|
|
|
|
except Exception as e:
|
2025-06-30 21:25:26 +03:00
|
|
|
|
logger.error(f"[decorators] Unexpected error in login_accepted: {e}")
|
|
|
|
|
|
msg = "Internal server error"
|
|
|
|
|
|
raise GraphQLError(msg) from e
|
|
|
|
|
|
|
|
|
|
|
|
return wrap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def editor_or_admin_required(func: Callable) -> Callable:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Декоратор для проверки, что пользователь имеет роль 'editor' или 'admin'.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
func: функция-резолвер для декорирования
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Callable: обернутая функция
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
|
async def wrap(parent: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Сначала проверяем авторизацию
|
|
|
|
|
|
await validate_graphql_context(info)
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем информацию о пользователе
|
|
|
|
|
|
request = info.context.get("request")
|
|
|
|
|
|
author_id = None
|
|
|
|
|
|
|
|
|
|
|
|
# Пробуем получить author_id из разных источников
|
|
|
|
|
|
if hasattr(request, "auth") and request.auth and hasattr(request.auth, "author_id"):
|
|
|
|
|
|
author_id = request.auth.author_id
|
|
|
|
|
|
elif hasattr(request, "scope") and "auth" in request.scope:
|
|
|
|
|
|
auth_info = request.scope.get("auth", {})
|
|
|
|
|
|
if isinstance(auth_info, dict):
|
|
|
|
|
|
author_id = auth_info.get("author_id")
|
|
|
|
|
|
elif hasattr(auth_info, "author_id"):
|
|
|
|
|
|
author_id = auth_info.author_id
|
|
|
|
|
|
|
|
|
|
|
|
if not author_id:
|
|
|
|
|
|
logger.warning("[decorators] Не удалось получить author_id для проверки ролей")
|
|
|
|
|
|
raise GraphQLError("Ошибка авторизации: не удалось определить пользователя")
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем роли пользователя
|
|
|
|
|
|
with local_session() as session:
|
2025-07-31 18:55:59 +03:00
|
|
|
|
author = session.query(Author).where(Author.id == author_id).first()
|
2025-06-30 21:25:26 +03:00
|
|
|
|
if not author:
|
|
|
|
|
|
logger.warning(f"[decorators] Автор с ID {author_id} не найден")
|
|
|
|
|
|
raise GraphQLError("Пользователь не найден")
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем email админа
|
|
|
|
|
|
if author.email in ADMIN_EMAILS:
|
|
|
|
|
|
logger.debug(f"[decorators] Пользователь {author.email} является админом по email")
|
|
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем список ролей пользователя
|
2025-07-02 22:30:21 +03:00
|
|
|
|
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
|
2025-07-31 18:55:59 +03:00
|
|
|
|
user_roles = ca.role_list if ca else []
|
2025-06-30 21:25:26 +03:00
|
|
|
|
logger.debug(f"[decorators] Роли пользователя {author_id}: {user_roles}")
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем наличие роли admin или editor
|
|
|
|
|
|
if "admin" in user_roles or "editor" in user_roles:
|
|
|
|
|
|
logger.debug(f"[decorators] Пользователь {author_id} имеет разрешение (роли: {user_roles})")
|
|
|
|
|
|
return await func(parent, info, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
# Если нет нужных ролей
|
|
|
|
|
|
logger.warning(f"[decorators] Пользователю {author_id} отказано в доступе. Роли: {user_roles}")
|
|
|
|
|
|
raise GraphQLError("Доступ запрещен. Требуется роль редактора или администратора.")
|
|
|
|
|
|
|
|
|
|
|
|
except GraphQLError:
|
|
|
|
|
|
# Пробрасываем ошибки авторизации далее
|
2025-05-21 18:29:32 +03:00
|
|
|
|
raise
|
2025-06-30 21:25:26 +03:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"[decorators] Неожиданная ошибка в editor_or_admin_required: {e}")
|
|
|
|
|
|
raise GraphQLError("Внутренняя ошибка сервера") from e
|
2025-05-20 00:00:24 +03:00
|
|
|
|
|
|
|
|
|
|
return wrap
|