2024-01-25 19:41:27 +00:00
|
|
|
|
from functools import wraps
|
2025-06-01 23:56:11 +00:00
|
|
|
|
from typing import Any, Callable, Optional
|
2024-04-08 07:38:58 +00:00
|
|
|
|
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from sqlalchemy import exc
|
2025-05-21 15:29:46 +00:00
|
|
|
|
from starlette.requests import Request
|
|
|
|
|
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from auth.internal import verify_internal_auth
|
2025-07-02 19:30:21 +00:00
|
|
|
|
from auth.orm import Author
|
2025-05-22 01:34:30 +00:00
|
|
|
|
from cache.cache import get_cached_author_by_id
|
2024-08-09 06:37:06 +00:00
|
|
|
|
from resolvers.stat import get_with_stat
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from services.db import local_session
|
2025-05-21 15:29:46 +00:00
|
|
|
|
from settings import SESSION_TOKEN_HEADER
|
2025-05-29 09:37:39 +00:00
|
|
|
|
from utils.logger import root_logger as logger
|
2024-01-13 08:49:12 +00:00
|
|
|
|
|
2024-12-24 11:04:52 +00:00
|
|
|
|
# Список разрешенных заголовков
|
2025-01-21 07:09:28 +00:00
|
|
|
|
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
|
|
|
|
|
2024-02-21 07:27:16 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def check_auth(req: Request) -> tuple[int, list[str], bool]:
|
2024-12-11 20:49:58 +00:00
|
|
|
|
"""
|
|
|
|
|
Проверка авторизации пользователя.
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
Проверяет токен и получает данные из локальной БД.
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
|
|
|
|
Параметры:
|
|
|
|
|
- req: Входящий GraphQL запрос, содержащий заголовок авторизации.
|
|
|
|
|
|
|
|
|
|
Возвращает:
|
2025-05-16 06:23:48 +00:00
|
|
|
|
- user_id: str - Идентификатор пользователя
|
|
|
|
|
- user_roles: list[str] - Список ролей пользователя
|
2025-05-20 22:34:02 +00:00
|
|
|
|
- is_admin: bool - Флаг наличия у пользователя административных прав
|
2024-12-11 20:49:58 +00:00
|
|
|
|
"""
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.debug("[check_auth] Проверка авторизации...")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-21 15:29:46 +00:00
|
|
|
|
# Получаем заголовок авторизации
|
|
|
|
|
token = None
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Если req is None (в тестах), возвращаем пустые данные
|
|
|
|
|
if not req:
|
|
|
|
|
logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)")
|
|
|
|
|
return 0, [], False
|
|
|
|
|
|
2025-05-21 15:29:46 +00:00
|
|
|
|
# Проверяем заголовок с учетом регистра
|
|
|
|
|
headers_dict = dict(req.headers.items())
|
|
|
|
|
logger.debug(f"[check_auth] Все заголовки: {headers_dict}")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-21 15:29:46 +00:00
|
|
|
|
# Ищем заголовок Authorization независимо от регистра
|
|
|
|
|
for header_name, header_value in headers_dict.items():
|
|
|
|
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
|
|
|
|
token = header_value
|
|
|
|
|
logger.debug(f"[check_auth] Найден заголовок {header_name}: {token[:10]}...")
|
|
|
|
|
break
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
if not token:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.debug("[check_auth] Токен не найден в заголовках")
|
|
|
|
|
return 0, [], False
|
2025-01-21 07:09:28 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Очищаем токен от префикса Bearer если он есть
|
|
|
|
|
if token.startswith("Bearer "):
|
|
|
|
|
token = token.split("Bearer ")[-1].strip()
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Проверяем авторизацию внутренним механизмом
|
2025-05-21 15:29:46 +00:00
|
|
|
|
logger.debug("[check_auth] Вызов verify_internal_auth...")
|
|
|
|
|
user_id, user_roles, is_admin = await verify_internal_auth(token)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
logger.debug(
|
|
|
|
|
f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}"
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-21 15:29:46 +00:00
|
|
|
|
# Если в ролях нет админа, но есть ID - проверяем в БД
|
|
|
|
|
if user_id and not is_admin:
|
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
# Преобразуем user_id в число
|
|
|
|
|
try:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
if isinstance(user_id, str):
|
|
|
|
|
user_id_int = int(user_id.strip())
|
|
|
|
|
else:
|
|
|
|
|
user_id_int = int(user_id)
|
2025-05-21 15:29:46 +00:00
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
|
|
|
|
else:
|
2025-07-02 19:30:21 +00:00
|
|
|
|
# Проверяем наличие админских прав через новую RBAC систему
|
|
|
|
|
from orm.community import get_user_roles_in_community
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
|
|
|
|
|
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
|
2025-05-21 15:29:46 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
return user_id, user_roles, is_admin
|
2023-10-23 14:47:11 +00:00
|
|
|
|
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def add_user_role(user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""
|
2025-07-02 19:30:21 +00:00
|
|
|
|
Добавление ролей пользователю в локальной БД через CommunityAuthor.
|
2024-01-25 19:41:27 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
Args:
|
|
|
|
|
user_id: ID пользователя
|
|
|
|
|
roles: Список ролей для добавления. По умолчанию ["author", "reader"]
|
2024-12-11 20:49:58 +00:00
|
|
|
|
"""
|
2025-05-16 06:23:48 +00:00
|
|
|
|
if not roles:
|
|
|
|
|
roles = ["author", "reader"]
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
logger.info(f"Adding roles {roles} to user {user_id}")
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
from orm.community import assign_role_to_user
|
|
|
|
|
|
|
|
|
|
logger.debug("Using local authentication with new RBAC system")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
with local_session() as session:
|
|
|
|
|
try:
|
|
|
|
|
author = session.query(Author).filter(Author.id == user_id).one()
|
2024-12-11 20:49:58 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
for role_name in roles:
|
2025-07-02 19:30:21 +00:00
|
|
|
|
success = assign_role_to_user(int(user_id), role_name, community_id=1)
|
|
|
|
|
if success:
|
|
|
|
|
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
return user_id
|
|
|
|
|
|
|
|
|
|
except exc.NoResultFound:
|
|
|
|
|
logger.error(f"Author {user_id} not found")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def login_required(f: Callable) -> Callable:
|
2025-05-20 22:34:02 +00:00
|
|
|
|
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
|
2024-12-11 22:04:11 +00:00
|
|
|
|
|
2024-01-23 18:59:46 +00:00
|
|
|
|
@wraps(f)
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
2025-05-20 22:34:02 +00:00
|
|
|
|
from graphql.error import GraphQLError
|
|
|
|
|
|
2023-10-23 14:47:11 +00:00
|
|
|
|
info = args[1]
|
2024-04-17 15:32:23 +00:00
|
|
|
|
req = info.context.get("request")
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.debug(
|
|
|
|
|
f"[login_required] Проверка авторизации для запроса: {req.method if req else 'unknown'} {req.url.path if req and hasattr(req, 'url') else 'unknown'}"
|
|
|
|
|
)
|
|
|
|
|
logger.debug(f"[login_required] Заголовки: {req.headers if req else 'none'}")
|
|
|
|
|
|
2025-06-02 18:50:58 +00:00
|
|
|
|
# Извлекаем токен из заголовков для сохранения в контексте
|
|
|
|
|
token = None
|
|
|
|
|
if req:
|
|
|
|
|
# Проверяем заголовок с учетом регистра
|
|
|
|
|
headers_dict = dict(req.headers.items())
|
|
|
|
|
|
|
|
|
|
# Ищем заголовок Authorization независимо от регистра
|
|
|
|
|
for header_name, header_value in headers_dict.items():
|
|
|
|
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
|
|
|
|
token = header_value
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"[login_required] Найден заголовок {header_name}: {token[:10] if token else 'None'}..."
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Очищаем токен от префикса Bearer если он есть
|
|
|
|
|
if token and token.startswith("Bearer "):
|
|
|
|
|
token = token.split("Bearer ")[-1].strip()
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Для тестового режима: если req отсутствует, но в контексте есть author и roles
|
|
|
|
|
if not req and info.context.get("author") and info.context.get("roles"):
|
|
|
|
|
logger.debug("[login_required] Тестовый режим: используем данные из контекста")
|
|
|
|
|
user_id = info.context["author"]["id"]
|
|
|
|
|
user_roles = info.context["roles"]
|
|
|
|
|
is_admin = info.context.get("is_admin", False)
|
2025-06-02 18:50:58 +00:00
|
|
|
|
# В тестовом режиме токен может быть в контексте
|
|
|
|
|
if not token:
|
|
|
|
|
token = info.context.get("token")
|
2025-06-01 23:56:11 +00:00
|
|
|
|
else:
|
|
|
|
|
# Обычный режим: проверяем через HTTP заголовки
|
|
|
|
|
user_id, user_roles, is_admin = await check_auth(req)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
if not user_id:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.debug(
|
|
|
|
|
f"[login_required] Пользователь не авторизован, req={dict(req) if req else 'None'}, info={info}"
|
|
|
|
|
)
|
|
|
|
|
msg = "Требуется авторизация"
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
# Проверяем наличие роли reader
|
2025-07-02 19:30:21 +00:00
|
|
|
|
if "reader" not in user_roles and not is_admin:
|
2025-05-20 22:34:02 +00:00
|
|
|
|
logger.error(f"Пользователь {user_id} не имеет роли 'reader'")
|
2025-06-01 23:56:11 +00:00
|
|
|
|
msg = "У вас нет необходимых прав для доступа"
|
|
|
|
|
raise GraphQLError(msg)
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
|
|
|
|
|
info.context["roles"] = user_roles
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
# Проверяем права администратора
|
|
|
|
|
info.context["is_admin"] = is_admin
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-06-02 18:50:58 +00:00
|
|
|
|
# Сохраняем токен в контексте для доступа в резолверах
|
|
|
|
|
if token:
|
|
|
|
|
info.context["token"] = token
|
|
|
|
|
logger.debug(f"[login_required] Токен сохранен в контексте: {token[:10] if token else 'None'}...")
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# В тестовом режиме автор уже может быть в контексте
|
|
|
|
|
if (
|
|
|
|
|
not info.context.get("author")
|
|
|
|
|
or not isinstance(info.context["author"], dict)
|
|
|
|
|
or "dict" not in str(type(info.context["author"]))
|
|
|
|
|
):
|
|
|
|
|
author = await get_cached_author_by_id(user_id, get_with_stat)
|
|
|
|
|
if not author:
|
|
|
|
|
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
|
|
|
|
info.context["author"] = author
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2024-02-27 11:16:54 +00:00
|
|
|
|
return await f(*args, **kwargs)
|
2023-10-23 14:47:11 +00:00
|
|
|
|
|
|
|
|
|
return decorated_function
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
def login_accepted(f: Callable) -> Callable:
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""Декоратор для добавления данных авторизации в контекст."""
|
2024-12-11 22:04:11 +00:00
|
|
|
|
|
2024-11-12 14:56:20 +00:00
|
|
|
|
@wraps(f)
|
2025-06-01 23:56:11 +00:00
|
|
|
|
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
2024-11-12 14:56:20 +00:00
|
|
|
|
info = args[1]
|
|
|
|
|
req = info.context.get("request")
|
2024-11-18 08:31:19 +00:00
|
|
|
|
|
2024-11-14 11:00:33 +00:00
|
|
|
|
logger.debug("login_accepted: Проверка авторизации пользователя.")
|
2025-05-20 22:34:02 +00:00
|
|
|
|
user_id, user_roles, is_admin = await check_auth(req)
|
2024-11-14 11:00:33 +00:00
|
|
|
|
logger.debug(f"login_accepted: user_id={user_id}, user_roles={user_roles}")
|
2024-11-18 08:31:19 +00:00
|
|
|
|
|
2024-11-12 14:56:20 +00:00
|
|
|
|
if user_id and user_roles:
|
2024-11-14 11:00:33 +00:00
|
|
|
|
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
|
2024-11-12 14:56:20 +00:00
|
|
|
|
info.context["roles"] = user_roles
|
2025-05-29 09:37:39 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
# Проверяем права администратора
|
|
|
|
|
info.context["is_admin"] = is_admin
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
|
|
|
|
# Пробуем получить профиль автора
|
2025-05-22 01:34:30 +00:00
|
|
|
|
author = await get_cached_author_by_id(user_id, get_with_stat)
|
2024-11-14 11:00:33 +00:00
|
|
|
|
if author:
|
|
|
|
|
logger.debug(f"login_accepted: Найден профиль автора: {author}")
|
2025-05-20 22:34:02 +00:00
|
|
|
|
# Используем флаг is_admin из контекста или передаем права владельца для собственных данных
|
|
|
|
|
is_owner = True # Пользователь всегда является владельцем собственного профиля
|
2025-06-01 23:56:11 +00:00
|
|
|
|
info.context["author"] = author.dict(is_owner or is_admin)
|
2024-11-14 11:00:33 +00:00
|
|
|
|
else:
|
2024-11-18 08:31:19 +00:00
|
|
|
|
logger.error(
|
2024-12-16 16:39:31 +00:00
|
|
|
|
f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные."
|
2025-05-16 06:23:48 +00:00
|
|
|
|
)
|
2024-11-12 14:56:20 +00:00
|
|
|
|
else:
|
2024-11-14 11:00:33 +00:00
|
|
|
|
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
|
2024-11-12 14:56:20 +00:00
|
|
|
|
info.context["roles"] = None
|
|
|
|
|
info.context["author"] = None
|
2025-05-20 22:34:02 +00:00
|
|
|
|
info.context["is_admin"] = False
|
|
|
|
|
|
|
|
|
|
return await f(*args, **kwargs)
|
2024-11-12 14:56:20 +00:00
|
|
|
|
|
2025-05-20 22:34:02 +00:00
|
|
|
|
return decorated_function
|