from functools import wraps from typing import Any, Callable, Optional from sqlalchemy import exc from starlette.requests import Request from auth.internal import verify_internal_auth from auth.orm import Author from cache.cache import get_cached_author_by_id from resolvers.stat import get_with_stat from services.db import local_session from settings import SESSION_TOKEN_HEADER from utils.logger import root_logger as logger # Список разрешенных заголовков ALLOWED_HEADERS = ["Authorization", "Content-Type"] async def check_auth(req: Request) -> tuple[int, list[str], bool]: """ Проверка авторизации пользователя. Проверяет токен и получает данные из локальной БД. Параметры: - req: Входящий GraphQL запрос, содержащий заголовок авторизации. Возвращает: - user_id: str - Идентификатор пользователя - user_roles: list[str] - Список ролей пользователя - is_admin: bool - Флаг наличия у пользователя административных прав """ logger.debug("[check_auth] Проверка авторизации...") # Получаем заголовок авторизации token = None # Если req is None (в тестах), возвращаем пустые данные if not req: logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)") return 0, [], False # Проверяем заголовок с учетом регистра headers_dict = dict(req.headers.items()) logger.debug(f"[check_auth] Все заголовки: {headers_dict}") # Ищем заголовок 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 if not token: logger.debug("[check_auth] Токен не найден в заголовках") return 0, [], False # Очищаем токен от префикса Bearer если он есть if token.startswith("Bearer "): token = token.split("Bearer ")[-1].strip() # Проверяем авторизацию внутренним механизмом logger.debug("[check_auth] Вызов verify_internal_auth...") user_id, user_roles, is_admin = await verify_internal_auth(token) logger.debug( f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}" ) # Если в ролях нет админа, но есть ID - проверяем в БД if user_id and not is_admin: try: with local_session() as session: # Преобразуем user_id в число try: if isinstance(user_id, str): user_id_int = int(user_id.strip()) else: user_id_int = int(user_id) except (ValueError, TypeError): logger.error(f"Невозможно преобразовать user_id {user_id} в число") else: # Проверяем наличие админских прав через новую RBAC систему from orm.community import get_user_roles_in_community 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) except Exception as e: logger.error(f"Ошибка при проверке прав администратора: {e}") return user_id, user_roles, is_admin async def add_user_role(user_id: str, roles: Optional[list[str]] = None) -> Optional[str]: """ Добавление ролей пользователю в локальной БД через CommunityAuthor. Args: user_id: ID пользователя roles: Список ролей для добавления. По умолчанию ["author", "reader"] """ if not roles: roles = ["author", "reader"] logger.info(f"Adding roles {roles} to user {user_id}") from orm.community import assign_role_to_user logger.debug("Using local authentication with new RBAC system") with local_session() as session: try: author = session.query(Author).filter(Author.id == user_id).one() # Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1) for role_name in roles: 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}") return user_id except exc.NoResultFound: logger.error(f"Author {user_id} not found") return None def login_required(f: Callable) -> Callable: """Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'.""" @wraps(f) async def decorated_function(*args: Any, **kwargs: Any) -> Any: from graphql.error import GraphQLError info = args[1] req = info.context.get("request") 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'}") # Извлекаем токен из заголовков для сохранения в контексте 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() # Для тестового режима: если 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) # В тестовом режиме токен может быть в контексте if not token: token = info.context.get("token") else: # Обычный режим: проверяем через HTTP заголовки user_id, user_roles, is_admin = await check_auth(req) if not user_id: logger.debug( f"[login_required] Пользователь не авторизован, req={dict(req) if req else 'None'}, info={info}" ) msg = "Требуется авторизация" raise GraphQLError(msg) # Проверяем наличие роли reader if "reader" not in user_roles and not is_admin: logger.error(f"Пользователь {user_id} не имеет роли 'reader'") msg = "У вас нет необходимых прав для доступа" raise GraphQLError(msg) logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}") info.context["roles"] = user_roles # Проверяем права администратора info.context["is_admin"] = is_admin # Сохраняем токен в контексте для доступа в резолверах if token: info.context["token"] = token logger.debug(f"[login_required] Токен сохранен в контексте: {token[:10] if token else 'None'}...") # В тестовом режиме автор уже может быть в контексте 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 return await f(*args, **kwargs) return decorated_function def login_accepted(f: Callable) -> Callable: """Декоратор для добавления данных авторизации в контекст.""" @wraps(f) async def decorated_function(*args: Any, **kwargs: Any) -> Any: info = args[1] req = info.context.get("request") logger.debug("login_accepted: Проверка авторизации пользователя.") user_id, user_roles, is_admin = await check_auth(req) logger.debug(f"login_accepted: user_id={user_id}, user_roles={user_roles}") if user_id and user_roles: logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}") info.context["roles"] = user_roles # Проверяем права администратора info.context["is_admin"] = is_admin # Пробуем получить профиль автора author = await get_cached_author_by_id(user_id, get_with_stat) if author: logger.debug(f"login_accepted: Найден профиль автора: {author}") # Используем флаг is_admin из контекста или передаем права владельца для собственных данных is_owner = True # Пользователь всегда является владельцем собственного профиля info.context["author"] = author.dict(is_owner or is_admin) else: logger.error( f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные." ) else: logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.") info.context["roles"] = None info.context["author"] = None info.context["is_admin"] = False return await f(*args, **kwargs) return decorated_function