This commit is contained in:
@@ -19,6 +19,12 @@ from services.env import EnvVariable, env_manager
|
||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# Отложенный импорт Author для избежания циклических импортов
|
||||
def get_author_model():
|
||||
"""Возвращает модель Author для использования в admin"""
|
||||
from auth.orm import Author
|
||||
return Author
|
||||
|
||||
|
||||
class AdminService:
|
||||
"""Сервис для админ-панели с бизнес-логикой"""
|
||||
@@ -53,6 +59,7 @@ class AdminService:
|
||||
"slug": "system",
|
||||
}
|
||||
|
||||
Author = get_author_model()
|
||||
author = session.query(Author).where(Author.id == author_id).first()
|
||||
if author:
|
||||
return {
|
||||
@@ -69,7 +76,7 @@ class AdminService:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_user_roles(user: Author, community_id: int = 1) -> list[str]:
|
||||
def get_user_roles(user: Any, community_id: int = 1) -> list[str]:
|
||||
"""Получает роли пользователя в сообществе"""
|
||||
|
||||
admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else []
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import secrets
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any, Callable
|
||||
|
||||
from graphql.error import GraphQLError
|
||||
from starlette.requests import Request
|
||||
@@ -21,6 +21,7 @@ from auth.orm import Author
|
||||
from auth.password import Password
|
||||
from auth.tokens.storage import TokenStorage
|
||||
from auth.tokens.verification import VerificationTokenManager
|
||||
from cache.cache import get_cached_author_by_id
|
||||
from orm.community import (
|
||||
Community,
|
||||
CommunityAuthor,
|
||||
@@ -38,6 +39,11 @@ from settings import (
|
||||
from utils.generate_slug import generate_unique_slug
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# Author уже импортирован в начале файла
|
||||
def get_author_model():
|
||||
"""Возвращает модель Author для использования в auth"""
|
||||
return Author
|
||||
|
||||
# Список разрешенных заголовков
|
||||
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
||||
|
||||
@@ -107,6 +113,7 @@ class AuthService:
|
||||
# Проверяем админские права через email если нет роли админа
|
||||
if not is_admin:
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
author = session.query(Author).where(Author.id == user_id_int).first()
|
||||
if author and author.email in ADMIN_EMAILS.split(","):
|
||||
is_admin = True
|
||||
@@ -120,7 +127,7 @@ class AuthService:
|
||||
|
||||
return user_id, user_roles, is_admin
|
||||
|
||||
async def add_user_role(self, user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
|
||||
async def add_user_role(self, user_id: str, roles: list[str] | None = None) -> str | None:
|
||||
"""
|
||||
Добавление ролей пользователю в локальной БД через CommunityAuthor.
|
||||
"""
|
||||
@@ -160,6 +167,7 @@ class AuthService:
|
||||
|
||||
# Проверяем уникальность email
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
existing_user = session.query(Author).where(Author.email == user_dict["email"]).first()
|
||||
if existing_user:
|
||||
# Если пользователь с таким email уже существует, возвращаем его
|
||||
@@ -172,6 +180,7 @@ class AuthService:
|
||||
# Проверяем уникальность slug
|
||||
with local_session() as session:
|
||||
# Добавляем суффикс, если slug уже существует
|
||||
Author = get_author_model()
|
||||
counter = 1
|
||||
unique_slug = base_slug
|
||||
while session.query(Author).where(Author.slug == unique_slug).first():
|
||||
@@ -227,9 +236,6 @@ class AuthService:
|
||||
|
||||
async def get_session(self, token: str) -> dict[str, Any]:
|
||||
"""Получает информацию о текущей сессии по токену"""
|
||||
# Поздний импорт для избежания циклических зависимостей
|
||||
from cache.cache import get_cached_author_by_id
|
||||
|
||||
try:
|
||||
# Проверяем токен
|
||||
payload = JWTCodec.decode(token)
|
||||
@@ -261,6 +267,7 @@ class AuthService:
|
||||
logger.info(f"Попытка регистрации для {email}")
|
||||
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
user = session.query(Author).where(Author.email == email).first()
|
||||
if user:
|
||||
logger.warning(f"Пользователь {email} уже существует")
|
||||
@@ -300,6 +307,7 @@ class AuthService:
|
||||
"""Отправляет ссылку подтверждения на email"""
|
||||
email = email.lower()
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
user = session.query(Author).where(Author.email == email).first()
|
||||
if not user:
|
||||
raise ObjectNotExistError("User not found")
|
||||
@@ -337,6 +345,7 @@ class AuthService:
|
||||
username = payload.get("username")
|
||||
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
user = session.query(Author).where(Author.id == user_id).first()
|
||||
if not user:
|
||||
logger.warning(f"Пользователь с ID {user_id} не найден")
|
||||
@@ -371,6 +380,7 @@ class AuthService:
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
Author = get_author_model()
|
||||
author = session.query(Author).where(Author.email == email).first()
|
||||
if not author:
|
||||
logger.warning(f"Пользователь {email} не найден")
|
||||
@@ -779,7 +789,6 @@ class AuthService:
|
||||
info.context["is_admin"] = is_admin
|
||||
|
||||
# Автор будет получен в резолвере при необходимости
|
||||
pass
|
||||
else:
|
||||
logger.debug("login_accepted: Пользователь не авторизован")
|
||||
info.context["roles"] = None
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
from graphql.error import GraphQLError
|
||||
|
||||
from auth.orm import Author
|
||||
# Импорт Author отложен для избежания циклических импортов
|
||||
from orm.community import Community
|
||||
from orm.draft import Draft
|
||||
from orm.reaction import Reaction
|
||||
@@ -11,6 +11,12 @@ from orm.shout import Shout
|
||||
from orm.topic import Topic
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# Отложенный импорт Author для избежания циклических импортов
|
||||
def get_author_model():
|
||||
"""Возвращает модель Author для использования в common_result"""
|
||||
from auth.orm import Author
|
||||
return Author
|
||||
|
||||
|
||||
def handle_error(operation: str, error: Exception) -> GraphQLError:
|
||||
"""Обрабатывает ошибки в резолверах"""
|
||||
@@ -28,8 +34,8 @@ class CommonResult:
|
||||
slugs: list[str] | None = None
|
||||
shout: Shout | None = None
|
||||
shouts: list[Shout] | None = None
|
||||
author: Author | None = None
|
||||
authors: list[Author] | None = None
|
||||
author: Any | None = None # Author type resolved at runtime
|
||||
authors: list[Any] | None = None # Author type resolved at runtime
|
||||
reaction: Reaction | None = None
|
||||
reactions: list[Reaction] | None = None
|
||||
topic: Topic | None = None
|
||||
|
||||
@@ -153,9 +153,8 @@ def create_table_if_not_exists(
|
||||
logger.info(f"Created table: {model_cls.__tablename__}")
|
||||
finally:
|
||||
# Close connection only if we created it
|
||||
if should_close:
|
||||
if hasattr(connection, "close"):
|
||||
connection.close() # type: ignore[attr-defined]
|
||||
if should_close and hasattr(connection, "close"):
|
||||
connection.close() # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def get_column_names_without_virtual(model_cls: Type[DeclarativeBase]) -> list[str]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Optional
|
||||
from typing import ClassVar
|
||||
|
||||
from services.redis import redis
|
||||
from utils.logger import root_logger as logger
|
||||
@@ -292,7 +292,7 @@ class EnvService:
|
||||
logger.error(f"Ошибка при удалении переменной {key}: {e}")
|
||||
return False
|
||||
|
||||
async def get_variable(self, key: str) -> Optional[str]:
|
||||
async def get_variable(self, key: str) -> str | None:
|
||||
"""Получает значение конкретной переменной"""
|
||||
|
||||
# Сначала проверяем Redis
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from collections.abc import Collection
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
|
||||
@@ -11,12 +11,12 @@ from services.redis import redis
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def save_notification(action: str, entity: str, payload: Union[dict[Any, Any], str, int, None]) -> None:
|
||||
def save_notification(action: str, entity: str, payload: dict[Any, Any] | str | int | None) -> None:
|
||||
"""Save notification with proper payload handling"""
|
||||
if payload is None:
|
||||
return
|
||||
|
||||
if isinstance(payload, (Reaction, Shout)):
|
||||
if isinstance(payload, Reaction | Shout):
|
||||
# Convert ORM objects to dict representation
|
||||
payload = {"id": payload.id}
|
||||
|
||||
@@ -26,7 +26,7 @@ def save_notification(action: str, entity: str, payload: Union[dict[Any, Any], s
|
||||
session.commit()
|
||||
|
||||
|
||||
async def notify_reaction(reaction: Union[Reaction, int], action: str = "create") -> None:
|
||||
async def notify_reaction(reaction: Reaction | int, action: str = "create") -> None:
|
||||
channel_name = "reaction"
|
||||
|
||||
# Преобразуем объект Reaction в словарь для сериализации
|
||||
@@ -56,7 +56,7 @@ async def notify_shout(shout: dict[str, Any], action: str = "update") -> None:
|
||||
data = {"payload": shout, "action": action}
|
||||
try:
|
||||
payload = data.get("payload")
|
||||
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
|
||||
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
|
||||
payload = str(payload)
|
||||
save_notification(action, channel_name, payload)
|
||||
await redis.publish(channel_name, orjson.dumps(data))
|
||||
@@ -72,7 +72,7 @@ async def notify_follower(follower: dict[str, Any], author_id: int, action: str
|
||||
data = {"payload": simplified_follower, "action": action}
|
||||
# save in channel
|
||||
payload = data.get("payload")
|
||||
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
|
||||
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
|
||||
payload = str(payload)
|
||||
save_notification(action, channel_name, payload)
|
||||
|
||||
@@ -144,7 +144,7 @@ async def notify_draft(draft_data: dict[str, Any], action: str = "publish") -> N
|
||||
|
||||
# Сохраняем уведомление
|
||||
payload = data.get("payload")
|
||||
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
|
||||
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
|
||||
payload = str(payload)
|
||||
save_notification(action, channel_name, payload)
|
||||
|
||||
|
||||
204
services/rbac.py
204
services/rbac.py
@@ -9,27 +9,15 @@ RBAC: динамическая система прав для ролей и со
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Any, Callable
|
||||
|
||||
from auth.orm import Author
|
||||
from auth.rbac_interface import get_community_queries, get_rbac_operations
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from settings import ADMIN_EMAILS
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# --- Загрузка каталога сущностей и дефолтных прав ---
|
||||
|
||||
with Path("services/permissions_catalog.json").open() as f:
|
||||
PERMISSIONS_CATALOG = json.load(f)
|
||||
|
||||
with Path("services/default_role_permissions.json").open() as f:
|
||||
DEFAULT_ROLE_PERMISSIONS = json.load(f)
|
||||
|
||||
role_names = list(DEFAULT_ROLE_PERMISSIONS.keys())
|
||||
|
||||
|
||||
async def initialize_community_permissions(community_id: int) -> None:
|
||||
"""
|
||||
@@ -38,117 +26,8 @@ async def initialize_community_permissions(community_id: int) -> None:
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
|
||||
# Проверяем, не инициализировано ли уже
|
||||
existing = await redis.execute("GET", key)
|
||||
if existing:
|
||||
logger.debug(f"Права для сообщества {community_id} уже инициализированы")
|
||||
return
|
||||
|
||||
# Создаем полные списки разрешений с учетом иерархии
|
||||
expanded_permissions = {}
|
||||
|
||||
def get_role_permissions(role: str, processed_roles: set[str] | None = None) -> set[str]:
|
||||
"""
|
||||
Рекурсивно получает все разрешения для роли, включая наследованные
|
||||
|
||||
Args:
|
||||
role: Название роли
|
||||
processed_roles: Список уже обработанных ролей для предотвращения зацикливания
|
||||
|
||||
Returns:
|
||||
Множество разрешений
|
||||
"""
|
||||
if processed_roles is None:
|
||||
processed_roles = set()
|
||||
|
||||
if role in processed_roles:
|
||||
return set()
|
||||
|
||||
processed_roles.add(role)
|
||||
|
||||
# Получаем прямые разрешения роли
|
||||
direct_permissions = set(DEFAULT_ROLE_PERMISSIONS.get(role, []))
|
||||
|
||||
# Проверяем, есть ли наследование роли
|
||||
for perm in list(direct_permissions):
|
||||
if perm in role_names:
|
||||
# Если пермишен - это название роли, добавляем все её разрешения
|
||||
direct_permissions.remove(perm)
|
||||
direct_permissions.update(get_role_permissions(perm, processed_roles))
|
||||
|
||||
return direct_permissions
|
||||
|
||||
# Формируем расширенные разрешения для каждой роли
|
||||
for role in role_names:
|
||||
expanded_permissions[role] = list(get_role_permissions(role))
|
||||
|
||||
# Сохраняем в Redis уже развернутые списки с учетом иерархии
|
||||
await redis.execute("SET", key, json.dumps(expanded_permissions))
|
||||
logger.info(f"Инициализированы права с иерархией для сообщества {community_id}")
|
||||
|
||||
|
||||
async def get_role_permissions_for_community(community_id: int) -> dict:
|
||||
"""
|
||||
Получает права ролей для конкретного сообщества.
|
||||
Если права не настроены, автоматически инициализирует их дефолтными.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Словарь прав ролей для сообщества
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
data = await redis.execute("GET", key)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
|
||||
# Автоматически инициализируем, если не найдено
|
||||
await initialize_community_permissions(community_id)
|
||||
|
||||
# Получаем инициализированные разрешения
|
||||
data = await redis.execute("GET", key)
|
||||
if data:
|
||||
return json.loads(data)
|
||||
|
||||
# Fallback на дефолтные разрешения если что-то пошло не так
|
||||
return DEFAULT_ROLE_PERMISSIONS
|
||||
|
||||
|
||||
async def set_role_permissions_for_community(community_id: int, role_permissions: dict) -> None:
|
||||
"""
|
||||
Устанавливает кастомные права ролей для сообщества.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
role_permissions: Словарь прав ролей
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
await redis.execute("SET", key, json.dumps(role_permissions))
|
||||
logger.info(f"Обновлены права ролей для сообщества {community_id}")
|
||||
|
||||
|
||||
async def update_all_communities_permissions() -> None:
|
||||
"""
|
||||
Обновляет права для всех существующих сообществ с новыми дефолтными настройками.
|
||||
"""
|
||||
from orm.community import Community
|
||||
|
||||
with local_session() as session:
|
||||
communities = session.query(Community).all()
|
||||
|
||||
for community in communities:
|
||||
# Удаляем старые права
|
||||
key = f"community:roles:{community.id}"
|
||||
await redis.execute("DEL", key)
|
||||
|
||||
# Инициализируем новые права
|
||||
await initialize_community_permissions(community.id)
|
||||
|
||||
logger.info(f"Обновлены права для {len(communities)} сообществ")
|
||||
rbac_ops = get_rbac_operations()
|
||||
await rbac_ops.initialize_community_permissions(community_id)
|
||||
|
||||
|
||||
async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
|
||||
@@ -163,42 +42,54 @@ async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
|
||||
Returns:
|
||||
Список разрешений для роли
|
||||
"""
|
||||
role_perms = await get_role_permissions_for_community(community_id)
|
||||
return role_perms.get(role, [])
|
||||
rbac_ops = get_rbac_operations()
|
||||
return await rbac_ops.get_permissions_for_role(role, community_id)
|
||||
|
||||
|
||||
async def update_all_communities_permissions() -> None:
|
||||
"""
|
||||
Обновляет права для всех существующих сообществ на основе актуальных дефолтных настроек.
|
||||
|
||||
Используется в админ-панели для применения изменений в правах на все сообщества.
|
||||
"""
|
||||
rbac_ops = get_rbac_operations()
|
||||
|
||||
# Поздний импорт для избежания циклических зависимостей
|
||||
from orm.community import Community
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Получаем все сообщества
|
||||
communities = session.query(Community).all()
|
||||
|
||||
for community in communities:
|
||||
# Сбрасываем кеш прав для каждого сообщества
|
||||
from services.redis import redis
|
||||
key = f"community:roles:{community.id}"
|
||||
await redis.execute("DEL", key)
|
||||
|
||||
# Переинициализируем права с актуальными дефолтными настройками
|
||||
await rbac_ops.initialize_community_permissions(community.id)
|
||||
|
||||
logger.info(f"Обновлены права для {len(communities)} сообществ")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении прав всех сообществ: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
# --- Получение ролей пользователя ---
|
||||
|
||||
|
||||
def get_user_roles_in_community(author_id: int, community_id: int = 1, session=None) -> list[str]:
|
||||
def get_user_roles_in_community(author_id: int, community_id: int = 1, session: Any = None) -> list[str]:
|
||||
"""
|
||||
Получает роли пользователя в сообществе через новую систему CommunityAuthor
|
||||
"""
|
||||
# Поздний импорт для избежания циклических зависимостей
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
try:
|
||||
if session:
|
||||
ca = (
|
||||
session.query(CommunityAuthor)
|
||||
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
return ca.role_list if ca else []
|
||||
# Используем local_session для продакшена
|
||||
with local_session() as db_session:
|
||||
ca = (
|
||||
db_session.query(CommunityAuthor)
|
||||
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
return ca.role_list if ca else []
|
||||
except Exception as e:
|
||||
logger.error(f"[get_user_roles_in_community] Ошибка при получении ролей: {e}")
|
||||
return []
|
||||
community_queries = get_community_queries()
|
||||
return community_queries.get_user_roles_in_community(author_id, community_id, session)
|
||||
|
||||
|
||||
async def user_has_permission(author_id: int, permission: str, community_id: int, session=None) -> bool:
|
||||
async def user_has_permission(author_id: int, permission: str, community_id: int, session: Any = None) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
|
||||
|
||||
@@ -211,8 +102,8 @@ async def user_has_permission(author_id: int, permission: str, community_id: int
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
user_roles = get_user_roles_in_community(author_id, community_id, session)
|
||||
return await roles_have_permission(user_roles, permission, community_id)
|
||||
rbac_ops = get_rbac_operations()
|
||||
return await rbac_ops.user_has_permission(author_id, permission, community_id, session)
|
||||
|
||||
|
||||
# --- Проверка прав ---
|
||||
@@ -228,8 +119,8 @@ async def roles_have_permission(role_slugs: list[str], permission: str, communit
|
||||
Returns:
|
||||
True если хотя бы одна роль имеет разрешение
|
||||
"""
|
||||
role_perms = await get_role_permissions_for_community(community_id)
|
||||
return any(permission in role_perms.get(role, []) for role in role_slugs)
|
||||
rbac_ops = get_rbac_operations()
|
||||
return await rbac_ops._roles_have_permission(role_slugs, permission, community_id)
|
||||
|
||||
|
||||
# --- Декораторы ---
|
||||
@@ -352,8 +243,7 @@ def get_community_id_from_context(info) -> int:
|
||||
if "slug" in variables:
|
||||
slug = variables["slug"]
|
||||
try:
|
||||
from orm.community import Community
|
||||
from services.db import local_session
|
||||
from orm.community import Community # Поздний импорт
|
||||
|
||||
with local_session() as session:
|
||||
community = session.query(Community).filter_by(slug=slug).first()
|
||||
|
||||
205
services/rbac_impl.py
Normal file
205
services/rbac_impl.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Реализация RBAC операций для использования через интерфейс.
|
||||
|
||||
Этот модуль предоставляет конкретную реализацию RBAC операций,
|
||||
не импортирует ORM модели напрямую, используя dependency injection.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from auth.orm import Author
|
||||
from auth.rbac_interface import CommunityAuthorQueries, RBACOperations, get_community_queries
|
||||
from services.db import local_session
|
||||
from services.redis import redis
|
||||
from settings import ADMIN_EMAILS
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# --- Загрузка каталога сущностей и дефолтных прав ---
|
||||
|
||||
with Path("services/permissions_catalog.json").open() as f:
|
||||
PERMISSIONS_CATALOG = json.load(f)
|
||||
|
||||
with Path("services/default_role_permissions.json").open() as f:
|
||||
DEFAULT_ROLE_PERMISSIONS = json.load(f)
|
||||
|
||||
role_names = list(DEFAULT_ROLE_PERMISSIONS.keys())
|
||||
|
||||
|
||||
class RBACOperationsImpl(RBACOperations):
|
||||
"""Конкретная реализация RBAC операций"""
|
||||
|
||||
async def get_permissions_for_role(self, role: str, community_id: int) -> list[str]:
|
||||
"""
|
||||
Получает список разрешений для конкретной роли в сообществе.
|
||||
Иерархия уже применена при инициализации сообщества.
|
||||
|
||||
Args:
|
||||
role: Название роли
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Список разрешений для роли
|
||||
"""
|
||||
role_perms = await self._get_role_permissions_for_community(community_id)
|
||||
return role_perms.get(role, [])
|
||||
|
||||
async def initialize_community_permissions(self, community_id: int) -> None:
|
||||
"""
|
||||
Инициализирует права для нового сообщества на основе дефолтных настроек с учетом иерархии.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
|
||||
# Проверяем, не инициализировано ли уже
|
||||
existing = await redis.execute("GET", key)
|
||||
if existing:
|
||||
logger.debug(f"Права для сообщества {community_id} уже инициализированы")
|
||||
return
|
||||
|
||||
# Создаем полные списки разрешений с учетом иерархии
|
||||
expanded_permissions = {}
|
||||
|
||||
def get_role_permissions(role: str, processed_roles: set[str] | None = None) -> set[str]:
|
||||
"""
|
||||
Рекурсивно получает все разрешения для роли, включая наследованные
|
||||
|
||||
Args:
|
||||
role: Название роли
|
||||
processed_roles: Список уже обработанных ролей для предотвращения зацикливания
|
||||
|
||||
Returns:
|
||||
Множество разрешений
|
||||
"""
|
||||
if processed_roles is None:
|
||||
processed_roles = set()
|
||||
|
||||
if role in processed_roles:
|
||||
return set()
|
||||
|
||||
processed_roles.add(role)
|
||||
|
||||
# Получаем прямые разрешения роли
|
||||
direct_permissions = set(DEFAULT_ROLE_PERMISSIONS.get(role, []))
|
||||
|
||||
# Проверяем, есть ли наследование роли
|
||||
for perm in list(direct_permissions):
|
||||
if perm in role_names:
|
||||
# Если пермишен - это название роли, добавляем все её разрешения
|
||||
direct_permissions.remove(perm)
|
||||
direct_permissions.update(get_role_permissions(perm, processed_roles))
|
||||
|
||||
return direct_permissions
|
||||
|
||||
# Формируем расширенные разрешения для каждой роли
|
||||
for role in role_names:
|
||||
expanded_permissions[role] = list(get_role_permissions(role))
|
||||
|
||||
# Сохраняем в Redis уже развернутые списки с учетом иерархии
|
||||
await redis.execute("SET", key, json.dumps(expanded_permissions))
|
||||
logger.info(f"Инициализированы права с иерархией для сообщества {community_id}")
|
||||
|
||||
async def user_has_permission(
|
||||
self, author_id: int, permission: str, community_id: int, session: Any = None
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
permission: Разрешение для проверки
|
||||
community_id: ID сообщества
|
||||
session: Опциональная сессия БД (для тестов)
|
||||
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
community_queries = get_community_queries()
|
||||
user_roles = community_queries.get_user_roles_in_community(author_id, community_id, session)
|
||||
return await self._roles_have_permission(user_roles, permission, community_id)
|
||||
|
||||
async def _get_role_permissions_for_community(self, community_id: int) -> dict:
|
||||
"""
|
||||
Получает права ролей для конкретного сообщества.
|
||||
Если права не настроены, автоматически инициализирует их дефолтными.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Словарь прав ролей для сообщества
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
data = await redis.execute("GET", key)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
|
||||
# Автоматически инициализируем, если не найдено
|
||||
await self.initialize_community_permissions(community_id)
|
||||
|
||||
# Получаем инициализированные разрешения
|
||||
data = await redis.execute("GET", key)
|
||||
if data:
|
||||
return json.loads(data)
|
||||
|
||||
# Fallback на дефолтные разрешения если что-то пошло не так
|
||||
return DEFAULT_ROLE_PERMISSIONS
|
||||
|
||||
async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у набора ролей конкретное разрешение в сообществе.
|
||||
|
||||
Args:
|
||||
role_slugs: Список ролей для проверки
|
||||
permission: Разрешение для проверки
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
True если хотя бы одна роль имеет разрешение
|
||||
"""
|
||||
role_perms = await self._get_role_permissions_for_community(community_id)
|
||||
return any(permission in role_perms.get(role, []) for role in role_slugs)
|
||||
|
||||
|
||||
class CommunityAuthorQueriesImpl(CommunityAuthorQueries):
|
||||
"""Конкретная реализация запросов CommunityAuthor через поздний импорт"""
|
||||
|
||||
def get_user_roles_in_community(
|
||||
self, author_id: int, community_id: int = 1, session: Any = None
|
||||
) -> list[str]:
|
||||
"""
|
||||
Получает роли пользователя в сообществе через новую систему CommunityAuthor
|
||||
"""
|
||||
# Поздний импорт для избежания циклических зависимостей
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
try:
|
||||
if session:
|
||||
ca = (
|
||||
session.query(CommunityAuthor)
|
||||
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
return ca.role_list if ca else []
|
||||
|
||||
# Используем local_session для продакшена
|
||||
with local_session() as db_session:
|
||||
ca = (
|
||||
db_session.query(CommunityAuthor)
|
||||
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
return ca.role_list if ca else []
|
||||
except Exception as e:
|
||||
logger.error(f"[get_user_roles_in_community] Ошибка при получении ролей: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Создаем экземпляры реализаций
|
||||
rbac_operations = RBACOperationsImpl()
|
||||
community_queries = CommunityAuthorQueriesImpl()
|
||||
24
services/rbac_init.py
Normal file
24
services/rbac_init.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Модуль инициализации RBAC системы.
|
||||
|
||||
Настраивает dependency injection для разрешения циклических зависимостей.
|
||||
Должен вызываться при старте приложения.
|
||||
"""
|
||||
|
||||
from auth.rbac_interface import set_community_queries, set_rbac_operations
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def initialize_rbac() -> None:
|
||||
"""
|
||||
Инициализирует RBAC систему с dependency injection.
|
||||
|
||||
Должна быть вызвана один раз при старте приложения после импорта всех модулей.
|
||||
"""
|
||||
from services.rbac_impl import community_queries, rbac_operations
|
||||
|
||||
# Устанавливаем реализации
|
||||
set_rbac_operations(rbac_operations)
|
||||
set_community_queries(community_queries)
|
||||
|
||||
logger.info("🧿 RBAC система инициализирована с dependency injection")
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional, Set, Union
|
||||
from typing import Any, Set
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
@@ -20,7 +20,7 @@ class RedisService:
|
||||
"""
|
||||
|
||||
def __init__(self, redis_url: str = REDIS_URL) -> None:
|
||||
self._client: Optional[aioredis.Redis] = None
|
||||
self._client: aioredis.Redis | None = None
|
||||
self._redis_url = redis_url # Исправлено на _redis_url
|
||||
self._is_available = aioredis is not None
|
||||
|
||||
@@ -126,11 +126,11 @@ class RedisService:
|
||||
logger.exception("Redis command failed")
|
||||
return None
|
||||
|
||||
async def get(self, key: str) -> Optional[Union[str, bytes]]:
|
||||
async def get(self, key: str) -> str | bytes | None:
|
||||
"""Get value by key"""
|
||||
return await self.execute("get", key)
|
||||
|
||||
async def set(self, key: str, value: Any, ex: Optional[int] = None) -> bool:
|
||||
async def set(self, key: str, value: Any, ex: int | None = None) -> bool:
|
||||
"""Set key-value pair with optional expiration"""
|
||||
if ex is not None:
|
||||
result = await self.execute("setex", key, ex, value)
|
||||
@@ -167,7 +167,7 @@ class RedisService:
|
||||
"""Set hash field"""
|
||||
await self.execute("hset", key, field, value)
|
||||
|
||||
async def hget(self, key: str, field: str) -> Optional[Union[str, bytes]]:
|
||||
async def hget(self, key: str, field: str) -> str | bytes | None:
|
||||
"""Get hash field"""
|
||||
return await self.execute("hget", key, field)
|
||||
|
||||
@@ -213,10 +213,10 @@ class RedisService:
|
||||
result = await self.execute("expire", key, seconds)
|
||||
return bool(result)
|
||||
|
||||
async def serialize_and_set(self, key: str, data: Any, ex: Optional[int] = None) -> bool:
|
||||
async def serialize_and_set(self, key: str, data: Any, ex: int | None = None) -> bool:
|
||||
"""Serialize data to JSON and store in Redis"""
|
||||
try:
|
||||
if isinstance(data, (str, bytes)):
|
||||
if isinstance(data, str | bytes):
|
||||
serialized_data: bytes = data.encode("utf-8") if isinstance(data, str) else data
|
||||
else:
|
||||
serialized_data = json.dumps(data).encode("utf-8")
|
||||
|
||||
@@ -9,9 +9,10 @@ from ariadne import (
|
||||
load_schema_from_path,
|
||||
)
|
||||
|
||||
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
|
||||
# Импорт Author, AuthorBookmark, AuthorFollower, AuthorRating отложен для избежания циклических импортов
|
||||
from orm import collection, community, draft, invite, notification, reaction, shout, topic
|
||||
from services.db import create_table_if_not_exists, local_session
|
||||
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
|
||||
|
||||
# Создаем основные типы
|
||||
query = QueryType()
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from httpx import AsyncClient, Response
|
||||
|
||||
@@ -80,7 +80,7 @@ class SearchCache:
|
||||
logger.info(f"Cached {len(results)} search results for query '{query}' in memory")
|
||||
return True
|
||||
|
||||
async def get(self, query: str, limit: int = 10, offset: int = 0) -> Optional[list]:
|
||||
async def get(self, query: str, limit: int = 10, offset: int = 0) -> list | None:
|
||||
"""Get paginated results for a query"""
|
||||
normalized_query = self._normalize_query(query)
|
||||
all_results = None
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Optional
|
||||
from typing import ClassVar
|
||||
|
||||
# ga
|
||||
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
||||
@@ -38,13 +38,13 @@ class ViewedStorage:
|
||||
shouts_by_author: ClassVar[dict] = {}
|
||||
views = None
|
||||
period = 60 * 60 # каждый час
|
||||
analytics_client: Optional[BetaAnalyticsDataClient] = None
|
||||
analytics_client: BetaAnalyticsDataClient | None = None
|
||||
auth_result = None
|
||||
running = False
|
||||
redis_views_key = None
|
||||
last_update_timestamp = 0
|
||||
start_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
_background_task: Optional[asyncio.Task] = None
|
||||
start_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
|
||||
_background_task: asyncio.Task | None = None
|
||||
|
||||
@staticmethod
|
||||
async def init() -> None:
|
||||
@@ -120,11 +120,11 @@ class ViewedStorage:
|
||||
timestamp = await redis.execute("HGET", latest_key, "_timestamp")
|
||||
if timestamp:
|
||||
self.last_update_timestamp = int(timestamp)
|
||||
timestamp_dt = datetime.fromtimestamp(int(timestamp), tz=timezone.utc)
|
||||
timestamp_dt = datetime.fromtimestamp(int(timestamp), tz=UTC)
|
||||
self.start_date = timestamp_dt.strftime("%Y-%m-%d")
|
||||
|
||||
# Если данные сегодняшние, считаем их актуальными
|
||||
now_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
now_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
|
||||
if now_date == self.start_date:
|
||||
logger.info(" * Views data is up to date!")
|
||||
else:
|
||||
@@ -291,7 +291,7 @@ class ViewedStorage:
|
||||
self.running = False
|
||||
break
|
||||
if failed == 0:
|
||||
when = datetime.now(timezone.utc) + timedelta(seconds=self.period)
|
||||
when = datetime.now(UTC) + timedelta(seconds=self.period)
|
||||
t = format(when.astimezone().isoformat())
|
||||
logger.info(" ⎩ next update: %s", t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
|
||||
await asyncio.sleep(self.period)
|
||||
|
||||
Reference in New Issue
Block a user