middleware-fix+oauth-routes
All checks were successful
Deploy on push / deploy (push) Successful in 6s

This commit is contained in:
2025-05-30 14:05:50 +03:00
parent f8ad73571c
commit f160ab4d26
5 changed files with 183 additions and 172 deletions

View File

@@ -1,19 +1,18 @@
"""
Утилитные функции для внутренней аутентификации
Используются в GraphQL резолверах и декораторах
"""
import time
from typing import Any, Optional, Tuple
from sqlalchemy.orm import exc
from starlette.authentication import AuthenticationBackend, BaseUser, UnauthenticatedUser
from starlette.requests import HTTPConnection
from auth.credentials import AuthCredentials
from auth.exceptions import ExpiredToken, InvalidToken
from auth.jwtcodec import JWTCodec
from auth.orm import Author
from auth.sessions import SessionManager
from auth.state import AuthState
from auth.tokenstorage import TokenStorage
from services.db import local_session
from services.redis import redis
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
from utils.logger import root_logger as logger
@@ -21,126 +20,6 @@ from utils.logger import root_logger as logger
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
class AuthenticatedUser(BaseUser):
"""Аутентифицированный пользователь для Starlette"""
def __init__(
self, user_id: str, username: str = "", roles: list = None, permissions: dict = None, token: str = None
):
self.user_id = user_id
self.username = username
self.roles = roles or []
self.permissions = permissions or {}
self.token = token
@property
def is_authenticated(self) -> bool:
return True
@property
def display_name(self) -> str:
return self.username
@property
def identity(self) -> str:
return self.user_id
class InternalAuthentication(AuthenticationBackend):
"""Внутренняя аутентификация через базу данных и Redis"""
async def authenticate(self, request: HTTPConnection):
"""
Аутентифицирует пользователя по токену из заголовка или cookie.
Порядок поиска токена:
1. Проверяем заголовок SESSION_TOKEN_HEADER (может быть установлен middleware)
2. Проверяем scope/auth в request, куда middleware мог сохранить токен
3. Проверяем cookie
Возвращает:
tuple: (AuthCredentials, BaseUser)
"""
token = None
# 1. Проверяем заголовок
if SESSION_TOKEN_HEADER in request.headers:
token_header = request.headers.get(SESSION_TOKEN_HEADER)
if token_header:
if token_header.startswith("Bearer "):
token = token_header.replace("Bearer ", "", 1).strip()
logger.debug(f"[auth.authenticate] Извлечен Bearer токен из заголовка {SESSION_TOKEN_HEADER}")
else:
token = token_header.strip()
logger.debug(f"[auth.authenticate] Извлечен прямой токен из заголовка {SESSION_TOKEN_HEADER}")
# 2. Проверяем scope/auth, который мог быть установлен middleware
if not token and hasattr(request, "scope") and "auth" in request.scope:
auth_data = request.scope.get("auth", {})
if isinstance(auth_data, dict) and "token" in auth_data:
token = auth_data["token"]
logger.debug(f"[auth.authenticate] Извлечен токен из request.scope['auth']")
# 3. Проверяем cookie
if not token and hasattr(request, "cookies") and SESSION_COOKIE_NAME in request.cookies:
token = request.cookies.get(SESSION_COOKIE_NAME)
logger.debug(f"[auth.authenticate] Извлечен токен из cookie {SESSION_COOKIE_NAME}")
# Если токен не найден, возвращаем неаутентифицированного пользователя
if not token:
logger.debug("[auth.authenticate] Токен не найден")
return AuthCredentials(scopes={}, error_message="no token"), UnauthenticatedUser()
# Проверяем сессию в Redis
payload = await SessionManager.verify_session(token)
if not payload:
logger.debug("[auth.authenticate] Недействительный токен")
return AuthCredentials(scopes={}, error_message="Invalid token"), UnauthenticatedUser()
with local_session() as session:
try:
author = (
session.query(Author)
.filter(Author.id == payload.user_id)
.filter(Author.is_active == True) # noqa
.one()
)
if author.is_locked():
logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}")
return AuthCredentials(scopes={}, error_message="Account is locked"), UnauthenticatedUser()
# Получаем разрешения из ролей
scopes = author.get_permissions()
# Получаем роли для пользователя
roles = [role.id for role in author.roles] if author.roles else []
# Обновляем last_seen
author.last_seen = int(time.time())
session.commit()
# Создаем объекты авторизации с сохранением токена
credentials = AuthCredentials(
author_id=author.id, scopes=scopes, logged_in=True, email=author.email, token=token
)
user = AuthenticatedUser(
user_id=str(author.id),
username=author.slug or author.email or "",
roles=roles,
permissions=scopes,
token=token,
)
logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}")
return credentials, user
except exc.NoResultFound:
logger.debug("[auth.authenticate] Пользователь не найден")
return AuthCredentials(scopes={}, error_message="User not found"), UnauthenticatedUser()
async def verify_internal_auth(token: str) -> Tuple[str, list, bool]:
"""
Проверяет локальную авторизацию.