refactored
Some checks failed
Deploy on push / deploy (push) Failing after 6s

This commit is contained in:
2025-08-17 17:56:31 +03:00
parent e78e12eeee
commit 9a2b792f08
98 changed files with 702 additions and 904 deletions

17
rbac/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
from 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 rbac.operations import community_queries, rbac_operations
# Устанавливаем реализации
set_rbac_operations(rbac_operations)
set_community_queries(community_queries)
logger.info("🧿 RBAC система инициализирована с dependency injection")

416
rbac/api.py Normal file
View File

@@ -0,0 +1,416 @@
"""
RBAC: динамическая система прав для ролей и сообществ.
- Каталог всех сущностей и действий хранится в permissions_catalog.json
- Дефолтные права ролей — в default_role_permissions.json
- Кастомные права ролей для каждого сообщества — в Redis (ключ community:roles:{community_id})
- При создании сообщества автоматически копируются дефолтные права
- Декораторы получают роли пользователя из CommunityAuthor для конкретного сообщества
"""
import asyncio
from functools import wraps
from typing import Any, Callable
from auth.orm import Author
from rbac.interface import get_community_queries, get_rbac_operations
from storage.db import local_session
from settings import ADMIN_EMAILS
from utils.logger import root_logger as logger
async def initialize_community_permissions(community_id: int) -> None:
"""
Инициализирует права для нового сообщества на основе дефолтных настроек с учетом иерархии.
Args:
community_id: ID сообщества
"""
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]:
"""
Получает список разрешений для конкретной роли в сообществе.
Иерархия уже применена при инициализации сообщества.
Args:
role: Название роли
community_id: ID сообщества
Returns:
Список разрешений для роли
"""
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 storage.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: Any = None) -> list[str]:
"""
Получает роли пользователя в сообществе через новую систему CommunityAuthor
"""
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: Any = None) -> bool:
"""
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
Args:
author_id: ID автора
permission: Разрешение для проверки
community_id: ID сообщества
session: Опциональная сессия БД (для тестов)
Returns:
True если разрешение есть, False если нет
"""
rbac_ops = get_rbac_operations()
return await rbac_ops.user_has_permission(author_id, permission, community_id, session)
# --- Проверка прав ---
async def roles_have_permission(role_slugs: list[str], permission: str, community_id: int) -> bool:
"""
Проверяет, есть ли у набора ролей конкретное разрешение в сообществе.
Args:
role_slugs: Список ролей для проверки
permission: Разрешение для проверки
community_id: ID сообщества
Returns:
True если хотя бы одна роль имеет разрешение
"""
rbac_ops = get_rbac_operations()
return await rbac_ops._roles_have_permission(role_slugs, permission, community_id)
# --- Декораторы ---
class RBACError(Exception):
"""Исключение для ошибок RBAC."""
def get_user_roles_from_context(info) -> tuple[list[str], int]:
"""
Получение ролей пользователя из GraphQL контекста с учетом сообщества.
Returns:
Кортеж (роли_пользователя, community_id)
"""
# Получаем ID автора из контекста
if isinstance(info.context, dict):
author_data = info.context.get("author", {})
else:
author_data = getattr(info.context, "author", {})
author_id = author_data.get("id") if isinstance(author_data, dict) else None
logger.debug(f"[get_user_roles_from_context] author_data: {author_data}, author_id: {author_id}")
# Если author_id не найден в context.author, пробуем получить из scope.auth
if not author_id and hasattr(info.context, "request"):
request = info.context.request
logger.debug(f"[get_user_roles_from_context] Проверяем request.scope: {hasattr(request, 'scope')}")
if hasattr(request, "scope") and "auth" in request.scope:
auth_credentials = request.scope["auth"]
logger.debug(f"[get_user_roles_from_context] Найден auth в scope: {type(auth_credentials)}")
if hasattr(auth_credentials, "author_id") and auth_credentials.author_id:
author_id = auth_credentials.author_id
logger.debug(f"[get_user_roles_from_context] Получен author_id из scope.auth: {author_id}")
elif isinstance(auth_credentials, dict) and "author_id" in auth_credentials:
author_id = auth_credentials["author_id"]
logger.debug(f"[get_user_roles_from_context] Получен author_id из scope.auth (dict): {author_id}")
else:
logger.debug("[get_user_roles_from_context] scope.auth не найден или пуст")
if hasattr(request, "scope"):
logger.debug(f"[get_user_roles_from_context] Ключи в scope: {list(request.scope.keys())}")
if not author_id:
logger.debug("[get_user_roles_from_context] author_id не найден ни в context.author, ни в scope.auth")
return [], 0
# Получаем community_id из аргументов мутации
community_id = get_community_id_from_context(info)
logger.debug(f"[get_user_roles_from_context] Получен community_id: {community_id}")
# Получаем роли пользователя в сообществе
try:
user_roles = get_user_roles_in_community(author_id, community_id)
logger.debug(
f"[get_user_roles_from_context] Роли пользователя {author_id} в сообществе {community_id}: {user_roles}"
)
# Проверяем, является ли пользователь системным администратором
try:
admin_emails = ADMIN_EMAILS.split(",") if ADMIN_EMAILS else []
with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first()
if author and author.email and author.email in admin_emails and "admin" not in user_roles:
# Системный администратор автоматически получает роль admin в любом сообществе
user_roles = [*user_roles, "admin"]
logger.debug(
f"[get_user_roles_from_context] Добавлена роль admin для системного администратора {author.email}"
)
except Exception as e:
logger.error(f"[get_user_roles_from_context] Ошибка при проверке системного администратора: {e}")
return user_roles, community_id
except Exception as e:
logger.error(f"[get_user_roles_from_context] Ошибка при получении ролей: {e}")
return [], community_id
def get_community_id_from_context(info) -> int:
"""
Получение community_id из GraphQL контекста или аргументов.
"""
# Пробуем из контекста
if isinstance(info.context, dict):
community_id = info.context.get("community_id")
else:
community_id = getattr(info.context, "community_id", None)
if community_id:
return int(community_id)
# Пробуем из аргументов resolver'а
logger.debug(
f"[get_community_id_from_context] Проверяем info.variable_values: {getattr(info, 'variable_values', None)}"
)
# Пробуем получить переменные из разных источников
variables = {}
# Способ 1: info.variable_values
if hasattr(info, "variable_values") and info.variable_values:
variables.update(info.variable_values)
logger.debug(f"[get_community_id_from_context] Добавлены переменные из variable_values: {info.variable_values}")
# Способ 2: info.variable_values (альтернативный способ)
if hasattr(info, "variable_values"):
logger.debug(f"[get_community_id_from_context] variable_values тип: {type(info.variable_values)}")
logger.debug(f"[get_community_id_from_context] variable_values содержимое: {info.variable_values}")
# Способ 3: из kwargs (аргументы функции)
if hasattr(info, "context") and hasattr(info.context, "kwargs"):
variables.update(info.context.kwargs)
logger.debug(f"[get_community_id_from_context] Добавлены переменные из context.kwargs: {info.context.kwargs}")
logger.debug(f"[get_community_id_from_context] Итоговые переменные: {variables}")
if "community_id" in variables:
return int(variables["community_id"])
if "communityId" in variables:
return int(variables["communityId"])
# Для мутации delete_community получаем slug и находим community_id
if "slug" in variables:
slug = variables["slug"]
try:
from orm.community import Community # Поздний импорт
with local_session() as session:
community = session.query(Community).filter_by(slug=slug).first()
if community:
logger.debug(f"[get_community_id_from_context] Найден community_id {community.id} для slug {slug}")
return community.id
logger.warning(f"[get_community_id_from_context] Сообщество с slug {slug} не найдено")
except Exception as e:
logger.exception(f"[get_community_id_from_context] Ошибка при поиске community_id: {e}")
# Пробуем из прямых аргументов
if hasattr(info, "field_asts") and info.field_asts:
for field_ast in info.field_asts:
if hasattr(field_ast, "arguments"):
for arg in field_ast.arguments:
if arg.name.value in ["community_id", "communityId"]:
return int(arg.value.value)
# Fallback: основное сообщество
logger.debug("[get_community_id_from_context] Используем дефолтный community_id: 1")
return 1
def require_permission(permission: str) -> Callable:
"""
Декоратор для проверки конкретного разрешения у пользователя в сообществе.
Args:
permission: Требуемое разрешение (например, "shout:create")
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else None
if not info or not hasattr(info, "context"):
raise RBACError("GraphQL info context не найден")
logger.debug(f"[require_permission] Проверяем права: {permission}")
logger.debug(f"[require_permission] args: {args}")
logger.debug(f"[require_permission] kwargs: {kwargs}")
user_roles, community_id = get_user_roles_from_context(info)
logger.debug(f"[require_permission] user_roles: {user_roles}, community_id: {community_id}")
has_permission = await roles_have_permission(user_roles, permission, community_id)
logger.debug(f"[require_permission] has_permission: {has_permission}")
if not has_permission:
raise RBACError("Недостаточно прав. Требуется: ", permission)
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
return wrapper
return decorator
def require_role(role: str) -> Callable:
"""
Декоратор для проверки конкретной роли у пользователя в сообществе.
Args:
role: Требуемая роль (например, "admin", "editor")
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else None
if not info or not hasattr(info, "context"):
raise RBACError("GraphQL info context не найден")
user_roles, community_id = get_user_roles_from_context(info)
if role not in user_roles:
raise RBACError("Требуется роль в сообществе", role)
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
return wrapper
return decorator
def require_any_permission(permissions: list[str]) -> Callable:
"""
Декоратор для проверки любого из списка разрешений.
Args:
permissions: Список разрешений, любое из которых подходит
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else None
if not info or not hasattr(info, "context"):
raise RBACError("GraphQL info context не найден")
user_roles, community_id = get_user_roles_from_context(info)
# Проверяем каждое разрешение отдельно
has_any = False
for perm in permissions:
if await roles_have_permission(user_roles, perm, community_id):
has_any = True
break
if not has_any:
raise RBACError("Недостаточно прав. Требуется любое из: ", permissions)
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
return wrapper
return decorator
def require_all_permissions(permissions: list[str]) -> Callable:
"""
Декоратор для проверки всех разрешений из списка.
Args:
permissions: Список разрешений, все из которых требуются
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else None
if not info or not hasattr(info, "context"):
raise RBACError("GraphQL info context не найден")
user_roles, community_id = get_user_roles_from_context(info)
# Проверяем каждое разрешение отдельно
missing_perms = []
for perm in permissions:
if not await roles_have_permission(user_roles, perm, community_id):
missing_perms.append(perm)
if missing_perms:
raise RBACError("Недостаточно прав. Отсутствуют: ", missing_perms)
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
return wrapper
return decorator
def admin_only(func: Callable) -> Callable:
"""
Декоратор для ограничения доступа только администраторам сообщества.
"""
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else None
if not info or not hasattr(info, "context"):
raise RBACError("GraphQL info context не найден")
user_roles, community_id = get_user_roles_from_context(info)
if "admin" not in user_roles:
raise RBACError("Доступ только для администраторов сообщества", community_id)
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
return wrapper

View File

@@ -0,0 +1,121 @@
{
"reader": [
"shout:read",
"topic:read",
"collection:read",
"community:read",
"bookmark:read",
"bookmark:create",
"bookmark:update",
"bookmark:delete",
"invite:read",
"invite:accept",
"invite:decline",
"chat:read",
"chat:create",
"chat:update",
"chat:delete",
"message:read",
"message:create",
"message:update",
"message:delete",
"reaction:read:COMMENT",
"reaction:create:COMMENT",
"reaction:update:COMMENT",
"reaction:delete:COMMENT",
"reaction:read:QUOTE",
"reaction:create:QUOTE",
"reaction:update:QUOTE",
"reaction:delete:QUOTE",
"reaction:read:LIKE",
"reaction:create:LIKE",
"reaction:update:LIKE",
"reaction:delete:LIKE",
"reaction:read:DISLIKE",
"reaction:create:DISLIKE",
"reaction:update:DISLIKE",
"reaction:delete:DISLIKE",
"reaction:read:CREDIT",
"reaction:read:PROOF",
"reaction:read:DISPROOF",
"reaction:read:AGREE",
"reaction:read:DISAGREE"
],
"author": [
"reader",
"draft:read",
"draft:create",
"draft:update",
"draft:delete",
"shout:create",
"shout:update",
"shout:delete",
"collection:create",
"collection:update",
"collection:delete",
"invite:create",
"invite:update",
"invite:delete",
"reaction:create:SILENT",
"reaction:read:SILENT",
"reaction:update:SILENT",
"reaction:delete:SILENT"
],
"artist": [
"author",
"reaction:create:CREDIT",
"reaction:read:CREDIT",
"reaction:update:CREDIT",
"reaction:delete:CREDIT"
],
"expert": [
"reader",
"reaction:create:PROOF",
"reaction:read:PROOF",
"reaction:update:PROOF",
"reaction:delete:PROOF",
"reaction:create:DISPROOF",
"reaction:read:DISPROOF",
"reaction:update:DISPROOF",
"reaction:delete:DISPROOF",
"reaction:create:AGREE",
"reaction:read:AGREE",
"reaction:update:AGREE",
"reaction:delete:AGREE",
"reaction:create:DISAGREE",
"reaction:read:DISAGREE",
"reaction:update:DISAGREE",
"reaction:delete:DISAGREE"
],
"editor": [
"author",
"shout:delete_any",
"shout:update_any",
"topic:create",
"topic:delete",
"topic:update",
"topic:merge",
"reaction:delete_any:*",
"reaction:update_any:*",
"invite:delete_any",
"invite:update_any",
"collection:delete_any",
"collection:update_any",
"community:create",
"community:update",
"community:delete",
"draft:delete_any",
"draft:update_any"
],
"admin": [
"editor",
"author:delete_any",
"author:update_any",
"chat:delete_any",
"chat:update_any",
"message:delete_any",
"message:update_any",
"community:delete_any",
"community:update_any"
]
}

75
rbac/interface.py Normal file
View File

@@ -0,0 +1,75 @@
"""
Интерфейс для RBAC операций, исключающий циркулярные импорты.
Этот модуль содержит только типы и абстрактные интерфейсы,
не импортирует ORM модели и не создает циклических зависимостей.
"""
from typing import Any, Protocol
class RBACOperations(Protocol):
"""
Протокол для RBAC операций, позволяющий ORM моделям
выполнять операции с правами без прямого импорта rbac.api
"""
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 # noqa: PLW0603
_rbac_operations = ops
def set_community_queries(queries: CommunityAuthorQueries) -> None:
"""Устанавливает реализацию запросов сообщества"""
global _community_queries # noqa: PLW0603
_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

200
rbac/operations.py Normal file
View File

@@ -0,0 +1,200 @@
"""
Реализация RBAC операций для использования через интерфейс.
Этот модуль предоставляет конкретную реализацию RBAC операций,
не импортирует ORM модели напрямую, используя dependency injection.
"""
import json
from pathlib import Path
from typing import Any
from rbac.interface import CommunityAuthorQueries, RBACOperations, get_community_queries
from storage.db import local_session
from storage.redis import redis
from utils.logger import root_logger as logger
# --- Загрузка каталога сущностей и дефолтных прав ---
with Path("rbac/permissions_catalog.json").open() as f:
PERMISSIONS_CATALOG = json.load(f)
with Path("rbac/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()

163
rbac/permissions.py Normal file
View File

@@ -0,0 +1,163 @@
"""
Модуль для проверки разрешений пользователей в контексте сообществ.
Позволяет проверять доступ пользователя к определенным операциям в сообществе
на основе его роли в этом сообществе.
"""
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

View File

@@ -0,0 +1,73 @@
{
"shout": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"topic": ["create", "read", "update_own", "update_any", "delete_own", "delete_any", "merge"],
"collection": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"bookmark": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"invite": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"chat": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"message": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"community": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"draft": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
"reaction": [
"create:LIKE",
"read:LIKE",
"update_own:LIKE",
"update_any:LIKE",
"delete_own:LIKE",
"delete_any:LIKE",
"create:COMMENT",
"read:COMMENT",
"update_own:COMMENT",
"update_any:COMMENT",
"delete_own:COMMENT",
"delete_any:COMMENT",
"create:QUOTE",
"read:QUOTE",
"update_own:QUOTE",
"update_any:QUOTE",
"delete_own:QUOTE",
"delete_any:QUOTE",
"create:DISLIKE",
"read:DISLIKE",
"update_own:DISLIKE",
"update_any:DISLIKE",
"delete_own:DISLIKE",
"delete_any:DISLIKE",
"create:CREDIT",
"read:CREDIT",
"update_own:CREDIT",
"update_any:CREDIT",
"delete_own:CREDIT",
"delete_any:CREDIT",
"create:PROOF",
"read:PROOF",
"update_own:PROOF",
"update_any:PROOF",
"delete_own:PROOF",
"delete_any:PROOF",
"create:DISPROOF",
"read:DISPROOF",
"update_own:DISPROOF",
"update_any:DISPROOF",
"delete_own:DISPROOF",
"delete_any:DISPROOF",
"create:AGREE",
"read:AGREE",
"update_own:AGREE",
"update_any:AGREE",
"delete_own:AGREE",
"delete_any:AGREE",
"create:DISAGREE",
"read:DISAGREE",
"update_own:DISAGREE",
"update_any:DISAGREE",
"delete_own:DISAGREE",
"delete_any:DISAGREE",
"create:SILENT",
"read:SILENT",
"update_own:SILENT",
"update_any:SILENT",
"delete_own:SILENT",
"delete_any:SILENT"
]
}