core/auth/decorators.py

143 lines
6.3 KiB
Python

from functools import wraps
from typing import Callable, Any
from graphql import GraphQLError
from services.db import local_session
from auth.orm import Author
from auth.exceptions import OperationNotAllowed
from utils.logger import root_logger as logger
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
def admin_auth_required(resolver: Callable) -> Callable:
"""
Декоратор для защиты админских эндпоинтов.
Проверяет принадлежность к списку разрешенных email-адресов.
Args:
resolver: GraphQL резолвер для защиты
Returns:
Обернутый резолвер, который проверяет права доступа
Raises:
GraphQLError: если пользователь не авторизован или не имеет доступа администратора
"""
@wraps(resolver)
async def wrapper(root: Any = None, info: Any = None, **kwargs):
try:
# Проверяем наличие info и контекста
if info is None or not hasattr(info, "context"):
logger.error("Missing GraphQL context information")
raise GraphQLError("Internal server error: missing context")
# Получаем ID пользователя из контекста запроса
request = info.context.get("request")
if not request or not hasattr(request, "auth"):
logger.error("Missing request or auth object in context")
raise GraphQLError("Internal server error: missing auth")
auth = request.auth
if not auth or not auth.logged_in:
client_info = {
"ip": request.client.host if hasattr(request, "client") else "unknown",
"headers": dict(request.headers),
}
logger.error(f"Unauthorized access attempt for admin endpoint: {client_info}")
raise GraphQLError("Unauthorized")
# Проверяем принадлежность к списку админов
with local_session() as session:
try:
author = session.query(Author).filter(Author.id == auth.author_id).one()
# Проверка по email
if author.email in ADMIN_EMAILS:
logger.info(
f"Admin access granted for {author.email} (special admin, ID: {author.id})"
)
return await resolver(root, info, **kwargs)
else:
logger.warning(
f"Admin access denied for {author.email} (ID: {author.id}) - not in admin list"
)
raise GraphQLError("Unauthorized - not an admin")
except Exception as db_error:
logger.error(f"Error fetching author with ID {auth.author_id}: {str(db_error)}")
raise GraphQLError("Unauthorized - user not found")
except Exception as e:
# Если ошибка уже GraphQLError, просто перебрасываем её
if isinstance(e, GraphQLError):
logger.error(f"GraphQL error in admin_auth_required: {str(e)}")
raise e
# Иначе, создаем новую GraphQLError
logger.error(f"Error in admin_auth_required: {str(e)}")
raise GraphQLError(f"Admin access error: {str(e)}")
return wrapper
def require_permission(permission_string: str):
"""
Декоратор для проверки наличия указанного разрешения.
Принимает строку в формате "resource:permission".
Args:
permission_string: Строка в формате "resource:permission"
Returns:
Декоратор, проверяющий наличие указанного разрешения
Raises:
ValueError: если строка разрешения имеет неверный формат
"""
if ":" not in permission_string:
raise ValueError('Permission string must be in format "resource:permission"')
resource, operation = permission_string.split(":", 1)
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(parent, info: Any = None, *args, **kwargs):
# Проверяем наличие info и контекста
if info is None or not hasattr(info, "context"):
logger.error("Missing GraphQL context information in require_permission")
raise OperationNotAllowed("Internal server error: missing context")
auth = info.context["request"].auth
if not auth or not auth.logged_in:
raise OperationNotAllowed("Unauthorized - please login")
with local_session() as session:
try:
author = session.query(Author).filter(Author.id == auth.author_id).one()
# Проверяем базовые условия
if not author.is_active:
raise OperationNotAllowed("Account is not active")
if author.is_locked():
raise OperationNotAllowed("Account is locked")
# Проверяем разрешение
if not author.has_permission(resource, operation):
logger.warning(
f"Access denied for user {auth.author_id} - no permission {resource}:{operation}"
)
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
# Пользователь аутентифицирован и имеет необходимое разрешение
return await func(parent, info, *args, **kwargs)
except Exception as e:
logger.error(f"Error in require_permission: {e}")
if isinstance(e, OperationNotAllowed):
raise e
raise OperationNotAllowed(str(e))
return wrapper
return decorator