core/auth/authenticate.py

134 lines
4.5 KiB
Python
Raw Normal View History

2023-10-26 20:38:31 +00:00
from functools import wraps
2025-05-16 06:23:48 +00:00
from typing import Optional
2023-10-26 21:07:35 +00:00
2023-10-26 20:38:31 +00:00
from graphql.type import GraphQLResolveInfo
2025-05-16 06:23:48 +00:00
from sqlalchemy.orm import exc
2023-10-26 20:38:31 +00:00
from starlette.authentication import AuthenticationBackend
from starlette.requests import HTTPConnection
2023-10-26 21:07:35 +00:00
2025-05-16 06:23:48 +00:00
from auth.credentials import AuthCredentials
2024-11-01 12:06:21 +00:00
from auth.exceptions import OperationNotAllowed
2025-05-16 06:23:48 +00:00
from auth.sessions import SessionManager
from auth.orm import Author
2024-11-01 12:06:21 +00:00
from services.db import local_session
2023-10-30 21:00:55 +00:00
from settings import SESSION_TOKEN_HEADER
2022-09-14 04:42:31 +00:00
class JWTAuthenticate(AuthenticationBackend):
2025-05-16 06:23:48 +00:00
async def authenticate(self, request: HTTPConnection) -> Optional[AuthCredentials]:
"""
Аутентификация пользователя по JWT токену.
Args:
request: HTTP запрос
Returns:
AuthCredentials при успешной аутентификации или None при ошибке
"""
2022-11-22 03:11:26 +00:00
if SESSION_TOKEN_HEADER not in request.headers:
2025-05-16 06:23:48 +00:00
return None
2022-09-14 04:42:31 +00:00
2025-05-16 06:23:48 +00:00
auth_header = request.headers.get(SESSION_TOKEN_HEADER)
if not auth_header:
2022-11-24 14:31:52 +00:00
print("[auth.authenticate] no token in header %s" % SESSION_TOKEN_HEADER)
2025-05-16 06:23:48 +00:00
return None
2022-11-22 03:11:26 +00:00
2025-05-16 06:23:48 +00:00
# Обработка формата "Bearer <token>"
token = auth_header
if auth_header.startswith("Bearer "):
token = auth_header.replace("Bearer ", "", 1).strip()
2023-07-13 14:58:22 +00:00
2025-05-16 06:23:48 +00:00
if not token:
print("[auth.authenticate] empty token after Bearer prefix removal")
return None
# Проверяем сессию в Redis
payload = await SessionManager.verify_session(token)
if not payload:
return None
2025-05-16 06:23:48 +00:00
with local_session() as session:
try:
author = (
session.query(Author)
.filter(Author.id == payload.user_id)
.filter(Author.is_active == True) # noqa
.one()
)
2025-05-16 06:23:48 +00:00
if author.is_locked():
return None
2025-05-16 06:23:48 +00:00
# Получаем разрешения из ролей
scopes = author.get_permissions()
return AuthCredentials(
author_id=author.id, scopes=scopes, logged_in=True, email=author.email
)
except exc.NoResultFound:
return None
2022-09-14 04:42:31 +00:00
def login_required(func):
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
auth: AuthCredentials = info.context["request"].auth
2022-11-28 14:15:54 +00:00
if not auth or not auth.logged_in:
2023-10-30 21:00:55 +00:00
return {"error": "Please login first"}
2022-09-14 04:42:31 +00:00
return await func(parent, info, *args, **kwargs)
return wrap
2022-11-24 08:27:01 +00:00
2025-05-16 06:23:48 +00:00
def permission_required(resource: str, operation: str, func):
"""
Декоратор для проверки разрешений.
Args:
resource (str): Ресурс для проверки
operation (str): Операция для проверки
func: Декорируемая функция
"""
2022-11-24 08:27:01 +00:00
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
auth: AuthCredentials = info.context["request"].auth
if not auth.logged_in:
2022-12-01 08:12:48 +00:00
raise OperationNotAllowed(auth.error_message or "Please login")
2022-11-24 08:27:01 +00:00
2025-05-16 06:23:48 +00:00
with local_session() as session:
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):
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
2022-11-24 08:27:01 +00:00
return await func(parent, info, *args, **kwargs)
return wrap
2024-11-12 14:56:20 +00:00
def login_accepted(func):
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
auth: AuthCredentials = info.context["request"].auth
if auth and auth.logged_in:
2025-05-16 06:23:48 +00:00
with local_session() as session:
author = session.query(Author).filter(Author.id == auth.author_id).one()
info.context["author"] = author.dict()
info.context["user_id"] = author.id
2024-11-12 14:56:20 +00:00
else:
info.context["author"] = None
info.context["user_id"] = None
return await func(parent, info, *args, **kwargs)
return wrap