This commit is contained in:
@@ -5,7 +5,7 @@ from starlette.responses import JSONResponse, RedirectResponse, Response
|
||||
from auth.core import verify_internal_auth
|
||||
from auth.orm import Author
|
||||
from auth.tokens.storage import TokenStorage
|
||||
from services.db import local_session
|
||||
from storage.db import local_session
|
||||
from settings import (
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
|
||||
13
auth/core.py
13
auth/core.py
@@ -4,12 +4,14 @@
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from auth.orm import Author
|
||||
from auth.state import AuthState
|
||||
from auth.tokens.storage import TokenStorage as TokenManager
|
||||
from auth.orm import Author
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
from storage.db import local_session
|
||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
@@ -50,7 +52,7 @@ async def verify_internal_auth(token: str) -> tuple[int, list, bool]:
|
||||
with local_session() as session:
|
||||
try:
|
||||
# Author уже импортирован в начале файла
|
||||
|
||||
|
||||
author = session.query(Author).where(Author.id == user_id).one()
|
||||
|
||||
# Получаем роли
|
||||
@@ -112,14 +114,14 @@ async def get_auth_token_from_request(request) -> str | None:
|
||||
"""
|
||||
# Отложенный импорт для избежания циклических зависимостей
|
||||
from auth.decorators import get_auth_token
|
||||
|
||||
|
||||
return await get_auth_token(request)
|
||||
|
||||
|
||||
async def authenticate(request) -> AuthState:
|
||||
"""
|
||||
Получает токен из запроса и проверяет авторизацию.
|
||||
|
||||
|
||||
Args:
|
||||
request: Объект запроса
|
||||
|
||||
@@ -146,4 +148,3 @@ async def authenticate(request) -> AuthState:
|
||||
auth_state.author_id = str(user_id)
|
||||
auth_state.is_admin = is_admin
|
||||
return auth_state
|
||||
|
||||
|
||||
@@ -5,28 +5,20 @@ from typing import Any
|
||||
from graphql import GraphQLError, GraphQLResolveInfo
|
||||
from sqlalchemy import exc
|
||||
|
||||
from auth.credentials import AuthCredentials
|
||||
from auth.exceptions import OperationNotAllowedError
|
||||
# Импорт базовых функций из реструктурированных модулей
|
||||
from auth.core import authenticate
|
||||
from auth.utils import get_auth_token
|
||||
from auth.credentials import AuthCredentials
|
||||
from auth.exceptions import OperationNotAllowedError
|
||||
from auth.orm import Author
|
||||
from auth.utils import get_auth_token, get_safe_headers
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.db import local_session
|
||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
||||
|
||||
|
||||
# Импортируем get_safe_headers из utils
|
||||
from auth.utils import get_safe_headers
|
||||
|
||||
|
||||
# get_auth_token теперь импортирован из auth.utils
|
||||
|
||||
|
||||
async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||
"""
|
||||
Проверяет валидность GraphQL контекста и проверяет авторизацию.
|
||||
|
||||
@@ -4,8 +4,8 @@ from auth.exceptions import ExpiredTokenError, InvalidPasswordError, InvalidToke
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.orm import Author
|
||||
from auth.password import Password
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from storage.db import local_session
|
||||
from storage.redis import redis
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
AuthorType = TypeVar("AuthorType", bound=Author)
|
||||
|
||||
@@ -7,7 +7,7 @@ DEPRECATED: Этот модуль переносится в auth/core.py
|
||||
"""
|
||||
|
||||
# Импорт базовых функций из core модуля
|
||||
from auth.core import verify_internal_auth, create_internal_session, authenticate
|
||||
from auth.core import authenticate, create_internal_session, verify_internal_auth
|
||||
|
||||
# Re-export для обратной совместимости
|
||||
__all__ = ["verify_internal_auth", "create_internal_session", "authenticate"]
|
||||
__all__ = ["authenticate", "create_internal_session", "verify_internal_auth"]
|
||||
|
||||
@@ -40,9 +40,7 @@ class JWTCodec:
|
||||
|
||||
# Если время истечения не указано, устанавливаем дефолтное
|
||||
if not expiration:
|
||||
expiration = datetime.datetime.now(datetime.UTC) + datetime.timedelta(
|
||||
days=JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||
)
|
||||
expiration = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=JWT_REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
logger.debug(f"[JWTCodec.encode] Время истечения не указано, устанавливаем срок: {expiration}")
|
||||
|
||||
# Формируем payload с временными метками
|
||||
|
||||
@@ -17,8 +17,8 @@ from starlette.types import ASGIApp
|
||||
from auth.credentials import AuthCredentials
|
||||
from auth.orm import Author
|
||||
from auth.tokens.storage import TokenStorage as TokenManager
|
||||
from services.db import local_session
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.db import local_session
|
||||
from storage.redis import redis as redis_adapter
|
||||
from settings import (
|
||||
ADMIN_EMAILS as ADMIN_EMAILS_LIST,
|
||||
)
|
||||
|
||||
@@ -13,8 +13,8 @@ from starlette.responses import JSONResponse, RedirectResponse
|
||||
from auth.orm import Author
|
||||
from auth.tokens.storage import TokenStorage
|
||||
from orm.community import Community, CommunityAuthor, CommunityFollower
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from storage.db import local_session
|
||||
from storage.redis import redis
|
||||
from settings import (
|
||||
FRONTEND_URL,
|
||||
OAUTH_CLIENTS,
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
"""
|
||||
Модуль для проверки разрешений пользователей в контексте сообществ.
|
||||
|
||||
Позволяет проверять доступ пользователя к определенным операциям в сообществе
|
||||
на основе его роли в этом сообществе.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.community import Community, CommunityAuthor
|
||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||
|
||||
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
||||
|
||||
|
||||
class ContextualPermissionCheck:
|
||||
"""
|
||||
Класс для проверки контекстно-зависимых разрешений.
|
||||
|
||||
Позволяет проверять разрешения пользователя в контексте сообщества,
|
||||
учитывая как глобальные роли пользователя, так и его роли внутри сообщества.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def check_community_permission(
|
||||
cls, session: Session, author_id: int, community_slug: str, resource: str, operation: str
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет наличие разрешения у пользователя в контексте сообщества.
|
||||
|
||||
Args:
|
||||
session: Сессия SQLAlchemy
|
||||
author_id: ID автора/пользователя
|
||||
community_slug: Slug сообщества
|
||||
resource: Ресурс для доступа
|
||||
operation: Операция над ресурсом
|
||||
|
||||
Returns:
|
||||
bool: True, если пользователь имеет разрешение, иначе False
|
||||
"""
|
||||
# 1. Проверка глобальных разрешений (например, администратор)
|
||||
author = session.query(Author).where(Author.id == author_id).one_or_none()
|
||||
if not author:
|
||||
return False
|
||||
# Если это администратор (по списку email)
|
||||
if author.email in ADMIN_EMAILS:
|
||||
return True
|
||||
|
||||
# 2. Проверка разрешений в контексте сообщества
|
||||
# Получаем информацию о сообществе
|
||||
community = session.query(Community).where(Community.slug == community_slug).one_or_none()
|
||||
if not community:
|
||||
return False
|
||||
|
||||
# Если автор является создателем сообщества, то у него есть полные права
|
||||
if community.created_by == author_id:
|
||||
return True
|
||||
|
||||
# Проверяем наличие разрешения для этих ролей
|
||||
permission_id = f"{resource}:{operation}"
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community.id, session)
|
||||
return bool(ca.has_permission(permission_id)) if ca else False
|
||||
|
||||
@classmethod
|
||||
def get_user_community_roles(cls, session: Session, author_id: int, community_slug: str) -> list[str]:
|
||||
"""
|
||||
Получает список ролей пользователя в сообществе.
|
||||
|
||||
Args:
|
||||
session: Сессия SQLAlchemy
|
||||
author_id: ID автора/пользователя
|
||||
community_slug: Slug сообщества
|
||||
|
||||
Returns:
|
||||
List[str]: Список ролей пользователя в сообществе
|
||||
"""
|
||||
# Получаем информацию о сообществе
|
||||
community = session.query(Community).where(Community.slug == community_slug).one_or_none()
|
||||
if not community:
|
||||
return []
|
||||
|
||||
# Если автор является создателем сообщества, то у него есть роль владельца
|
||||
if community.created_by == author_id:
|
||||
return ["editor", "author", "expert", "reader"]
|
||||
|
||||
# Находим связь автор-сообщество
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community.id, session)
|
||||
return ca.role_list if ca else []
|
||||
|
||||
@classmethod
|
||||
def check_permission(
|
||||
cls, session: Session, author_id: int, community_slug: str, resource: str, operation: str
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет наличие разрешения у пользователя в контексте сообщества.
|
||||
Синхронный метод для обратной совместимости.
|
||||
|
||||
Args:
|
||||
session: Сессия SQLAlchemy
|
||||
author_id: ID автора/пользователя
|
||||
community_slug: Slug сообщества
|
||||
resource: Ресурс для доступа
|
||||
operation: Операция над ресурсом
|
||||
|
||||
Returns:
|
||||
bool: True, если пользователь имеет разрешение, иначе False
|
||||
"""
|
||||
# Используем тот же алгоритм, что и в асинхронной версии
|
||||
author = session.query(Author).where(Author.id == author_id).one_or_none()
|
||||
if not author:
|
||||
return False
|
||||
# Если это администратор (по списку email)
|
||||
if author.email in ADMIN_EMAILS:
|
||||
return True
|
||||
|
||||
# Получаем информацию о сообществе
|
||||
community = session.query(Community).where(Community.slug == community_slug).one_or_none()
|
||||
if not community:
|
||||
return False
|
||||
|
||||
# Если автор является создателем сообщества, то у него есть полные права
|
||||
if community.created_by == author_id:
|
||||
return True
|
||||
|
||||
# Проверяем наличие разрешения для этих ролей
|
||||
permission_id = f"{resource}:{operation}"
|
||||
ca = CommunityAuthor.find_author_in_community(author_id, community.id, session)
|
||||
|
||||
# Возвращаем результат проверки разрешения
|
||||
return bool(ca and ca.has_permission(permission_id))
|
||||
|
||||
async def can_delete_community(self, user_id: int, community: Community, session: Session) -> bool:
|
||||
"""
|
||||
Проверяет, может ли пользователь удалить сообщество.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
community: Объект сообщества
|
||||
session: Сессия SQLAlchemy
|
||||
|
||||
Returns:
|
||||
bool: True, если пользователь может удалить сообщество, иначе False
|
||||
"""
|
||||
# Если пользователь - создатель сообщества
|
||||
if community.created_by == user_id:
|
||||
return True
|
||||
|
||||
# Проверяем, есть ли у пользователя роль администратора или редактора
|
||||
author = session.query(Author).where(Author.id == user_id).first()
|
||||
if not author:
|
||||
return False
|
||||
|
||||
# Проверка по email (глобальные администраторы)
|
||||
if author.email in ADMIN_EMAILS:
|
||||
return True
|
||||
|
||||
# Проверка ролей в сообществе
|
||||
community_author = CommunityAuthor.find_author_in_community(user_id, community.id, session)
|
||||
if community_author:
|
||||
return "admin" in community_author.role_list or "editor" in community_author.role_list
|
||||
|
||||
return False
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
Интерфейс для RBAC операций, исключающий циркулярные импорты.
|
||||
|
||||
Этот модуль содержит только типы и абстрактные интерфейсы,
|
||||
не импортирует ORM модели и не создает циклических зависимостей.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class RBACOperations(Protocol):
|
||||
"""
|
||||
Протокол для RBAC операций, позволяющий ORM моделям
|
||||
выполнять операции с правами без прямого импорта services.rbac
|
||||
"""
|
||||
|
||||
async def get_permissions_for_role(self, role: str, community_id: int) -> list[str]:
|
||||
"""Получает разрешения для роли в сообществе"""
|
||||
...
|
||||
|
||||
async def initialize_community_permissions(self, community_id: int) -> None:
|
||||
"""Инициализирует права для нового сообщества"""
|
||||
...
|
||||
|
||||
async def user_has_permission(
|
||||
self, author_id: int, permission: str, community_id: int, session: Any = None
|
||||
) -> bool:
|
||||
"""Проверяет разрешение пользователя в сообществе"""
|
||||
...
|
||||
|
||||
async def _roles_have_permission(
|
||||
self, role_slugs: list[str], permission: str, community_id: int
|
||||
) -> bool:
|
||||
"""Проверяет, есть ли у набора ролей конкретное разрешение в сообществе"""
|
||||
...
|
||||
|
||||
|
||||
class CommunityAuthorQueries(Protocol):
|
||||
"""
|
||||
Протокол для запросов CommunityAuthor, позволяющий RBAC
|
||||
выполнять запросы без прямого импорта ORM моделей
|
||||
"""
|
||||
|
||||
def get_user_roles_in_community(
|
||||
self, author_id: int, community_id: int, session: Any = None
|
||||
) -> list[str]:
|
||||
"""Получает роли пользователя в сообществе"""
|
||||
...
|
||||
|
||||
|
||||
# Глобальные переменные для dependency injection
|
||||
_rbac_operations: RBACOperations | None = None
|
||||
_community_queries: CommunityAuthorQueries | None = None
|
||||
|
||||
|
||||
def set_rbac_operations(ops: RBACOperations) -> None:
|
||||
"""Устанавливает реализацию RBAC операций"""
|
||||
global _rbac_operations
|
||||
_rbac_operations = ops
|
||||
|
||||
|
||||
def set_community_queries(queries: CommunityAuthorQueries) -> None:
|
||||
"""Устанавливает реализацию запросов сообщества"""
|
||||
global _community_queries
|
||||
_community_queries = queries
|
||||
|
||||
|
||||
def get_rbac_operations() -> RBACOperations:
|
||||
"""Получает реализацию RBAC операций"""
|
||||
if _rbac_operations is None:
|
||||
raise RuntimeError("RBAC operations не инициализированы. Вызовите set_rbac_operations()")
|
||||
return _rbac_operations
|
||||
|
||||
|
||||
def get_community_queries() -> CommunityAuthorQueries:
|
||||
"""Получает реализацию запросов сообщества"""
|
||||
if _community_queries is None:
|
||||
raise RuntimeError("Community queries не инициализированы. Вызовите set_community_queries()")
|
||||
return _community_queries
|
||||
@@ -3,7 +3,6 @@
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class AuthState:
|
||||
"""
|
||||
Класс для хранения информации о состоянии авторизации пользователя.
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.redis import redis as redis_adapter
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from .base import BaseTokenManager
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict
|
||||
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.redis import redis as redis_adapter
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from .base import BaseTokenManager
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.redis import redis as redis_adapter
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from .base import BaseTokenManager
|
||||
@@ -84,9 +84,7 @@ class OAuthTokenManager(BaseTokenManager):
|
||||
return await self._get_oauth_data_optimized(token_type, str(user_id), provider)
|
||||
return None
|
||||
|
||||
async def _get_oauth_data_optimized(
|
||||
self, token_type: TokenType, user_id: str, provider: str
|
||||
) -> TokenData | None:
|
||||
async def _get_oauth_data_optimized(self, token_type: TokenType, user_id: str, provider: str) -> TokenData | None:
|
||||
"""Оптимизированное получение OAuth данных"""
|
||||
if not user_id or not provider:
|
||||
error_msg = "OAuth токены требуют user_id и provider"
|
||||
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
from typing import Any, List
|
||||
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.redis import redis as redis_adapter
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from .base import BaseTokenManager
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from services.redis import redis as redis_adapter
|
||||
from storage.redis import redis as redis_adapter
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
from .base import BaseTokenManager
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
@@ -113,26 +114,25 @@ async def get_auth_token(request: Any) -> str | None:
|
||||
token = auth_header.replace("Bearer ", "", 1).strip()
|
||||
logger.debug(f"[decorators] Извлечен Bearer токен: {len(token)}")
|
||||
return token
|
||||
else:
|
||||
logger.debug("[decorators] Authorization заголовок не содержит Bearer токен")
|
||||
logger.debug("[decorators] Authorization заголовок не содержит Bearer токен")
|
||||
|
||||
# 6. Проверяем cookies
|
||||
if hasattr(request, "cookies") and request.cookies:
|
||||
if isinstance(request.cookies, dict):
|
||||
cookies = request.cookies
|
||||
elif hasattr(request.cookies, "get"):
|
||||
cookies = {k: request.cookies.get(k) for k in getattr(request.cookies, "keys", lambda: [])()}
|
||||
cookies = {k: request.cookies.get(k) for k in getattr(request.cookies, "keys", list)()}
|
||||
else:
|
||||
cookies = {}
|
||||
|
||||
|
||||
logger.debug(f"[decorators] Доступные cookies: {list(cookies.keys())}")
|
||||
|
||||
|
||||
# Проверяем кастомную cookie
|
||||
if SESSION_COOKIE_NAME in cookies:
|
||||
token = cookies[SESSION_COOKIE_NAME]
|
||||
logger.debug(f"[decorators] Токен найден в cookie {SESSION_COOKIE_NAME}: {len(token)}")
|
||||
return token
|
||||
|
||||
|
||||
# Проверяем стандартную cookie
|
||||
if "auth_token" in cookies:
|
||||
token = cookies["auth_token"]
|
||||
@@ -150,29 +150,29 @@ async def get_auth_token(request: Any) -> str | None:
|
||||
def extract_bearer_token(auth_header: str) -> str | None:
|
||||
"""
|
||||
Извлекает токен из заголовка Authorization с Bearer схемой.
|
||||
|
||||
|
||||
Args:
|
||||
auth_header: Заголовок Authorization
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[str]: Извлеченный токен или None
|
||||
"""
|
||||
if not auth_header:
|
||||
return None
|
||||
|
||||
|
||||
if auth_header.startswith("Bearer "):
|
||||
return auth_header[7:].strip()
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def format_auth_header(token: str) -> str:
|
||||
"""
|
||||
Форматирует токен в заголовок Authorization.
|
||||
|
||||
|
||||
Args:
|
||||
token: Токен авторизации
|
||||
|
||||
|
||||
Returns:
|
||||
str: Отформатированный заголовок
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user