This commit is contained in:
@@ -5,7 +5,7 @@ from sqlalchemy import exc
|
||||
from starlette.requests import Request
|
||||
|
||||
from auth.internal import verify_internal_auth
|
||||
from auth.orm import Author, Role
|
||||
from auth.orm import Author
|
||||
from cache.cache import get_cached_author_by_id
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.db import local_session
|
||||
@@ -79,15 +79,11 @@ async def check_auth(req: Request) -> tuple[int, list[str], bool]:
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
||||
else:
|
||||
# Проверяем наличие админских прав через БД
|
||||
from auth.orm import AuthorRole
|
||||
# Проверяем наличие админских прав через новую RBAC систему
|
||||
from orm.community import get_user_roles_in_community
|
||||
|
||||
admin_role = (
|
||||
session.query(AuthorRole)
|
||||
.filter(AuthorRole.author == user_id_int, AuthorRole.role.in_(["admin", "super"]))
|
||||
.first()
|
||||
)
|
||||
is_admin = admin_role is not None
|
||||
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
|
||||
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
||||
|
||||
@@ -96,7 +92,7 @@ async def check_auth(req: Request) -> tuple[int, list[str], bool]:
|
||||
|
||||
async def add_user_role(user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
|
||||
"""
|
||||
Добавление ролей пользователю в локальной БД.
|
||||
Добавление ролей пользователю в локальной БД через CommunityAuthor.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
@@ -107,27 +103,21 @@ async def add_user_role(user_id: str, roles: Optional[list[str]] = None) -> Opti
|
||||
|
||||
logger.info(f"Adding roles {roles} to user {user_id}")
|
||||
|
||||
logger.debug("Using local authentication")
|
||||
from orm.community import assign_role_to_user
|
||||
|
||||
logger.debug("Using local authentication with new RBAC system")
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == user_id).one()
|
||||
|
||||
# Получаем существующие роли
|
||||
existing_roles = {role.name for role in author.roles}
|
||||
|
||||
# Добавляем новые роли
|
||||
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
|
||||
for role_name in roles:
|
||||
if role_name not in existing_roles:
|
||||
# Получаем или создаем роль
|
||||
role = session.query(Role).filter(Role.name == role_name).first()
|
||||
if not role:
|
||||
role = Role(id=role_name, name=role_name)
|
||||
session.add(role)
|
||||
success = assign_role_to_user(int(user_id), role_name, community_id=1)
|
||||
if success:
|
||||
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
|
||||
else:
|
||||
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
|
||||
|
||||
# Добавляем роль автору
|
||||
author.roles.append(role)
|
||||
|
||||
session.commit()
|
||||
return user_id
|
||||
|
||||
except exc.NoResultFound:
|
||||
@@ -190,7 +180,7 @@ def login_required(f: Callable) -> Callable:
|
||||
raise GraphQLError(msg)
|
||||
|
||||
# Проверяем наличие роли reader
|
||||
if "reader" not in user_roles:
|
||||
if "reader" not in user_roles and not is_admin:
|
||||
logger.error(f"Пользователь {user_id} не имеет роли 'reader'")
|
||||
msg = "У вас нет необходимых прав для доступа"
|
||||
raise GraphQLError(msg)
|
||||
|
369
services/rbac.py
Normal file
369
services/rbac.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
RBAC: динамическая система прав для ролей и сообществ.
|
||||
|
||||
- Каталог всех сущностей и действий хранится в permissions_catalog.json
|
||||
- Дефолтные права ролей — в default_role_permissions.json
|
||||
- Кастомные права ролей для каждого сообщества — в Redis (ключ community:roles:{community_id})
|
||||
- При создании сообщества автоматически копируются дефолтные права
|
||||
- Декораторы получают роли пользователя из CommunityAuthor для конкретного сообщества
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Callable, List
|
||||
|
||||
from services.redis import redis
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
# --- Загрузка каталога сущностей и дефолтных прав ---
|
||||
|
||||
with Path("permissions_catalog.json").open() as f:
|
||||
PERMISSIONS_CATALOG = json.load(f)
|
||||
|
||||
with Path("default_role_permissions.json").open() as f:
|
||||
DEFAULT_ROLE_PERMISSIONS = json.load(f)
|
||||
|
||||
DEFAULT_ROLES_HIERARCHY: dict[str, list[str]] = {
|
||||
"reader": [], # Базовая роль, ничего не наследует
|
||||
"author": ["reader"], # Наследует от reader
|
||||
"artist": ["reader", "author"], # Наследует от reader и author
|
||||
"expert": ["reader", "author", "artist"], # Наследует от reader и author
|
||||
"editor": ["reader", "author", "artist", "expert"], # Наследует от reader и author
|
||||
"admin": ["reader", "author", "artist", "expert", "editor"], # Наследует от всех
|
||||
}
|
||||
|
||||
|
||||
# --- Инициализация и управление правами сообщества ---
|
||||
|
||||
|
||||
async def initialize_community_permissions(community_id: int) -> None:
|
||||
"""
|
||||
Инициализирует права для нового сообщества на основе дефолтных настроек с учетом иерархии.
|
||||
|
||||
Args:
|
||||
community_id: ID сообщества
|
||||
"""
|
||||
key = f"community:roles:{community_id}"
|
||||
|
||||
# Проверяем, не инициализировано ли уже
|
||||
existing = await redis.get(key)
|
||||
if existing:
|
||||
logger.debug(f"Права для сообщества {community_id} уже инициализированы")
|
||||
return
|
||||
|
||||
# Создаем полные списки разрешений с учетом иерархии
|
||||
expanded_permissions = {}
|
||||
|
||||
for role, direct_permissions in DEFAULT_ROLE_PERMISSIONS.items():
|
||||
# Начинаем с прямых разрешений роли
|
||||
all_permissions = set(direct_permissions)
|
||||
|
||||
# Добавляем наследуемые разрешения
|
||||
inherited_roles = DEFAULT_ROLES_HIERARCHY.get(role, [])
|
||||
for inherited_role in inherited_roles:
|
||||
inherited_permissions = DEFAULT_ROLE_PERMISSIONS.get(inherited_role, [])
|
||||
all_permissions.update(inherited_permissions)
|
||||
|
||||
expanded_permissions[role] = list(all_permissions)
|
||||
|
||||
# Сохраняем в Redis уже развернутые списки с учетом иерархии
|
||||
await redis.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.get(key)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
|
||||
# Автоматически инициализируем, если не найдено
|
||||
await initialize_community_permissions(community_id)
|
||||
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.set(key, json.dumps(role_permissions))
|
||||
logger.info(f"Обновлены права ролей для сообщества {community_id}")
|
||||
|
||||
|
||||
async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
|
||||
"""
|
||||
Получает список разрешений для конкретной роли в сообществе.
|
||||
Иерархия уже применена при инициализации сообщества.
|
||||
|
||||
Args:
|
||||
role: Название роли
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Список разрешений для роли
|
||||
"""
|
||||
role_perms = await get_role_permissions_for_community(community_id)
|
||||
return role_perms.get(role, [])
|
||||
|
||||
|
||||
# --- Получение ролей пользователя ---
|
||||
|
||||
|
||||
def get_user_roles_in_community(author_id: int, community_id: int) -> list[str]:
|
||||
"""
|
||||
Получает роли пользователя в конкретном сообществе из CommunityAuthor.
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
Список ролей пользователя в сообществе
|
||||
"""
|
||||
from orm.community import CommunityAuthor
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
ca = (
|
||||
session.query(CommunityAuthor)
|
||||
.filter(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
return ca.role_list if ca else []
|
||||
|
||||
|
||||
async def user_has_permission(author_id: int, permission: str, community_id: int) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
|
||||
|
||||
Args:
|
||||
author_id: ID автора
|
||||
permission: Разрешение для проверки
|
||||
community_id: ID сообщества
|
||||
|
||||
Returns:
|
||||
True если разрешение есть, False если нет
|
||||
"""
|
||||
user_roles = get_user_roles_in_community(author_id, community_id)
|
||||
return await roles_have_permission(user_roles, permission, community_id)
|
||||
|
||||
|
||||
# --- Проверка прав ---
|
||||
async def roles_have_permission(role_slugs: list[str], permission: str, community_id: int) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у набора ролей конкретное разрешение в сообществе.
|
||||
|
||||
Args:
|
||||
role_slugs: Список ролей для проверки
|
||||
permission: Разрешение для проверки
|
||||
community_id: ID сообщества
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# --- Декораторы ---
|
||||
class RBACError(Exception):
|
||||
"""Исключение для ошибок RBAC."""
|
||||
|
||||
|
||||
def get_user_roles_from_context(info) -> tuple[list[str], int]:
|
||||
"""
|
||||
Получение ролей пользователя из GraphQL контекста с учетом сообщества.
|
||||
|
||||
Returns:
|
||||
Кортеж (роли_пользователя, community_id)
|
||||
"""
|
||||
# Получаем ID автора из контекста
|
||||
author_data = getattr(info.context, "author", {})
|
||||
author_id = author_data.get("id") if isinstance(author_data, dict) else None
|
||||
|
||||
if not author_id:
|
||||
return [], 1
|
||||
|
||||
# Получаем community_id
|
||||
community_id = get_community_id_from_context(info)
|
||||
|
||||
# Получаем роли пользователя в этом сообществе
|
||||
user_roles = get_user_roles_in_community(author_id, community_id)
|
||||
|
||||
return user_roles, community_id
|
||||
|
||||
|
||||
def get_community_id_from_context(info) -> int:
|
||||
"""
|
||||
Получение community_id из GraphQL контекста или аргументов.
|
||||
"""
|
||||
# Пробуем из контекста
|
||||
community_id = getattr(info.context, "community_id", None)
|
||||
if community_id:
|
||||
return int(community_id)
|
||||
|
||||
# Пробуем из аргументов resolver'а
|
||||
if hasattr(info, "variable_values") and info.variable_values:
|
||||
if "community_id" in info.variable_values:
|
||||
return int(info.variable_values["community_id"])
|
||||
if "communityId" in info.variable_values:
|
||||
return int(info.variable_values["communityId"])
|
||||
|
||||
# Пробуем из прямых аргументов
|
||||
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: основное сообщество
|
||||
return 1
|
||||
|
||||
|
||||
def require_permission(permission: str):
|
||||
"""
|
||||
Декоратор для проверки конкретного разрешения у пользователя в сообществе.
|
||||
|
||||
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 не найден")
|
||||
|
||||
user_roles, community_id = get_user_roles_from_context(info)
|
||||
if not await roles_have_permission(user_roles, permission, community_id):
|
||||
raise RBACError("Недостаточно прав в сообществе")
|
||||
|
||||
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_role(role: str):
|
||||
"""
|
||||
Декоратор для проверки конкретной роли у пользователя в сообществе.
|
||||
|
||||
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]):
|
||||
"""
|
||||
Декоратор для проверки любого из списка разрешений.
|
||||
|
||||
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 = any(await roles_have_permission(user_roles, perm, community_id) for perm in permissions)
|
||||
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]):
|
||||
"""
|
||||
Декоратор для проверки всех разрешений из списка.
|
||||
|
||||
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 = [
|
||||
perm for perm in permissions if not await roles_have_permission(user_roles, perm, community_id)
|
||||
]
|
||||
|
||||
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
|
@@ -1,16 +1,36 @@
|
||||
from asyncio.log import logger
|
||||
from typing import List
|
||||
from enum import Enum
|
||||
|
||||
from ariadne import MutationType, ObjectType, QueryType, SchemaBindable
|
||||
from ariadne import (
|
||||
MutationType,
|
||||
ObjectType,
|
||||
QueryType,
|
||||
SchemaBindable,
|
||||
load_schema_from_path,
|
||||
)
|
||||
|
||||
from services.db import create_table_if_not_exists, local_session
|
||||
|
||||
# Создаем основные типы
|
||||
query = QueryType()
|
||||
mutation = MutationType()
|
||||
type_draft = ObjectType("Draft")
|
||||
type_community = ObjectType("Community")
|
||||
type_collection = ObjectType("Collection")
|
||||
resolvers: List[SchemaBindable] = [query, mutation, type_draft, type_community, type_collection]
|
||||
type_author = ObjectType("Author")
|
||||
|
||||
# Загружаем определения типов из файлов схемы
|
||||
type_defs = load_schema_from_path("schema/")
|
||||
|
||||
# Список всех типов для схемы
|
||||
resolvers: SchemaBindable | type[Enum] | list[SchemaBindable | type[Enum]] = [
|
||||
query,
|
||||
mutation,
|
||||
type_draft,
|
||||
type_community,
|
||||
type_collection,
|
||||
type_author,
|
||||
]
|
||||
|
||||
|
||||
def create_all_tables() -> None:
|
||||
|
Reference in New Issue
Block a user