Squashed new RBAC
All checks were successful
Deploy on push / deploy (push) Successful in 7s

This commit is contained in:
2025-07-02 22:30:21 +03:00
parent 7585dae0ab
commit 82111ed0f6
100 changed files with 14785 additions and 5888 deletions

View File

@@ -42,7 +42,7 @@ class AuthCredentials(BaseModel):
result = []
for resource, operations in self.scopes.items():
for operation in operations:
result.append(f"{resource}:{operation}")
result.extend([f"{resource}:{operation}"])
return result
def has_permission(self, resource: str, operation: str) -> bool:
@@ -71,18 +71,19 @@ class AuthCredentials(BaseModel):
"""
return self.email in ADMIN_EMAILS if self.email else False
def to_dict(self) -> dict[str, Any]:
async def to_dict(self) -> dict[str, Any]:
"""
Преобразует учетные данные в словарь
Returns:
Dict[str, Any]: Словарь с данными учетных данных
"""
permissions = self.get_permissions()
return {
"author_id": self.author_id,
"logged_in": self.logged_in,
"is_admin": self.is_admin,
"permissions": self.get_permissions(),
"permissions": list(permissions),
}
async def permissions(self) -> list[Permission]:

View File

@@ -9,6 +9,7 @@ from auth.credentials import AuthCredentials
from auth.exceptions import OperationNotAllowed
from auth.internal import authenticate
from auth.orm import Author
from orm.community import CommunityAuthor
from services.db import local_session
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
@@ -165,25 +166,24 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
# Проверяем auth из контекста - если уже авторизован, просто возвращаем
auth = getattr(request, "auth", None)
if auth and auth.logged_in:
if auth and getattr(auth, "logged_in", False):
logger.debug(f"[validate_graphql_context] Пользователь уже авторизован через request.auth: {auth.author_id}")
return
# Если аутентификации нет в request.auth, пробуем получить ее из scope
if hasattr(request, "scope") and "auth" in request.scope:
auth_cred = request.scope.get("auth")
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
if isinstance(auth_cred, AuthCredentials) and getattr(auth_cred, "logged_in", False):
logger.debug(f"[validate_graphql_context] Пользователь авторизован через scope: {auth_cred.author_id}")
# Больше не устанавливаем request.auth напрямую
return
# Если авторизации нет ни в auth, ни в scope, пробуем получить и проверить токен
token = get_auth_token(request)
if not token:
# Если токен не найден, возвращаем ошибку авторизации
# Если токен не найден, бросаем ошибку авторизации
client_info = {
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
"headers": get_safe_headers(request),
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
}
logger.warning(f"[validate_graphql_context] Токен авторизации не найден: {client_info}")
msg = "Unauthorized - please login"
@@ -211,7 +211,7 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
logger.debug(f"[validate_graphql_context] Найден автор: id={author.id}, email={author.email}")
# Получаем разрешения из ролей
scopes = author.get_permissions()
scopes = await author.get_permissions()
# Создаем объект авторизации
auth_cred = AuthCredentials(
@@ -231,6 +231,8 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
)
else:
logger.error("[validate_graphql_context] Не удалось установить auth: отсутствует request.scope")
msg = "Internal server error: unable to set authentication context"
raise GraphQLError(msg)
except exc.NoResultFound:
logger.error(f"[validate_graphql_context] Пользователь с ID {auth_state.author_id} не найден в базе данных")
msg = "Unauthorized - user not found"
@@ -261,94 +263,86 @@ def admin_auth_required(resolver: Callable) -> Callable:
@wraps(resolver)
async def wrapper(root: Any = None, info: Optional[GraphQLResolveInfo] = None, **kwargs: dict[str, Any]) -> Any:
# Подробное логирование для диагностики
logger.debug(f"[admin_auth_required] Начало проверки авторизации для {resolver.__name__}")
# Проверяем авторизацию пользователя
if info is None:
logger.error("[admin_auth_required] GraphQL info is None")
msg = "Invalid GraphQL context"
raise GraphQLError(msg)
# Логируем детали запроса
request = info.context.get("request")
client_info = {
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
}
logger.debug(f"[admin_auth_required] Детали запроса: {client_info}")
# Проверяем наличие токена до validate_graphql_context
token = get_auth_token(request)
logger.debug(f"[admin_auth_required] Токен найден: {bool(token)}, длина: {len(token) if token else 0}")
try:
# Подробное логирование для диагностики
logger.debug(f"[admin_auth_required] Начало проверки авторизации для {resolver.__name__}")
# Проверяем авторизацию пользователя
if info is None:
logger.error("[admin_auth_required] GraphQL info is None")
msg = "Invalid GraphQL context"
raise GraphQLError(msg)
# Логируем детали запроса
request = info.context.get("request")
client_info = {
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
}
logger.debug(f"[admin_auth_required] Детали запроса: {client_info}")
# Проверяем наличие токена до validate_graphql_context
token = get_auth_token(request)
logger.debug(f"[admin_auth_required] Токен найден: {bool(token)}, длина: {len(token) if token else 0}")
# Проверяем авторизацию
# Проверяем авторизацию - НЕ ловим GraphQLError здесь!
await validate_graphql_context(info)
logger.debug("[admin_auth_required] validate_graphql_context успешно пройден")
except GraphQLError:
# Пробрасываем GraphQLError дальше - это ошибки авторизации
logger.debug("[admin_auth_required] GraphQLError от validate_graphql_context - пробрасываем дальше")
raise
if info:
# Получаем объект авторизации
auth = None
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
auth = info.context["request"].scope.get("auth")
logger.debug(f"[admin_auth_required] Auth из scope: {auth.author_id if auth else None}")
elif hasattr(info.context["request"], "auth"):
auth = info.context["request"].auth
logger.debug(f"[admin_auth_required] Auth из request: {auth.author_id if auth else None}")
else:
logger.error("[admin_auth_required] Auth не найден ни в scope, ни в request")
# Получаем объект авторизации
auth = None
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
auth = info.context["request"].scope.get("auth")
logger.debug(f"[admin_auth_required] Auth из scope: {auth.author_id if auth else None}")
elif hasattr(info.context["request"], "auth"):
auth = info.context["request"].auth
logger.debug(f"[admin_auth_required] Auth из request: {auth.author_id if auth else None}")
else:
logger.error("[admin_auth_required] Auth не найден ни в scope, ни в request")
if not auth or not getattr(auth, "logged_in", False):
logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
msg = "Unauthorized - please login"
if not auth or not getattr(auth, "logged_in", False):
logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
msg = "Unauthorized - please login"
raise GraphQLError(msg)
# Проверяем, является ли пользователь администратором
try:
with local_session() as session:
# Преобразуем author_id в int для совместимости с базой данных
author_id = int(auth.author_id) if auth and auth.author_id else None
if not author_id:
logger.error(f"[admin_auth_required] ID автора не определен: {auth}")
msg = "Unauthorized - invalid user ID"
raise GraphQLError(msg)
# Проверяем, является ли пользователь администратором
with local_session() as session:
try:
# Преобразуем author_id в int для совместимости с базой данных
author_id = int(auth.author_id) if auth and auth.author_id else None
if not author_id:
logger.error(f"[admin_auth_required] ID автора не определен: {auth}")
msg = "Unauthorized - invalid user ID"
raise GraphQLError(msg)
author = session.query(Author).filter(Author.id == author_id).one()
logger.debug(f"[admin_auth_required] Найден автор: {author.id}, {author.email}")
author = session.query(Author).filter(Author.id == author_id).one()
logger.debug(f"[admin_auth_required] Найден автор: {author.id}, {author.email}")
# Проверяем, является ли пользователь системным администратором
if author.email and author.email in ADMIN_EMAILS:
logger.info(f"System admin access granted for {author.email} (ID: {author.id})")
return await resolver(root, info, **kwargs)
# Проверяем, является ли пользователь администратором
if author.email in ADMIN_EMAILS:
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
return await resolver(root, info, **kwargs)
# Проверяем роли пользователя
admin_roles = ["admin", "super"]
user_roles = [role.id for role in author.roles] if author.roles else []
logger.debug(f"[admin_auth_required] Роли пользователя: {user_roles}")
if any(role in admin_roles for role in user_roles):
logger.info(
f"Admin access granted for {author.email} (ID: {author.id}) with role: {user_roles}"
)
return await resolver(root, info, **kwargs)
logger.warning(f"Admin access denied for {author.email} (ID: {author.id}). Roles: {user_roles}")
msg = "Unauthorized - not an admin"
raise GraphQLError(msg)
except exc.NoResultFound:
logger.error(
f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных"
)
msg = "Unauthorized - user not found"
raise GraphQLError(msg) from None
# Системный администратор определяется ТОЛЬКО по ADMIN_EMAILS
logger.warning(f"System admin access denied for {author.email} (ID: {author.id}). Not in ADMIN_EMAILS.")
msg = "Unauthorized - system admin access required"
raise GraphQLError(msg)
except exc.NoResultFound:
logger.error(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
msg = "Unauthorized - user not found"
raise GraphQLError(msg) from None
except GraphQLError:
# Пробрасываем GraphQLError дальше
raise
except Exception as e:
error_msg = str(e)
if not isinstance(e, GraphQLError):
error_msg = f"Admin access error: {error_msg}"
logger.error(f"Error in admin_auth_required: {error_msg}")
logger.error(f"[admin_auth_required] Ошибка авторизации: {error_msg}")
# Ловим только неожиданные ошибки, не GraphQLError
error_msg = f"Admin access error: {e!s}"
logger.error(f"[admin_auth_required] Неожиданная ошибка: {error_msg}")
raise GraphQLError(error_msg) from e
return wrapper
@@ -396,7 +390,11 @@ def permission_required(resource: str, operation: str, func: Callable) -> Callab
# Проверяем роли пользователя
admin_roles = ["admin", "super"]
user_roles = [role.id for role in author.roles] if author.roles else []
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
if ca:
user_roles = ca.role_list
else:
user_roles = []
if any(role in admin_roles for role in user_roles):
logger.debug(
@@ -499,7 +497,11 @@ def editor_or_admin_required(func: Callable) -> Callable:
return await func(parent, info, *args, **kwargs)
# Получаем список ролей пользователя
user_roles = [role.id for role in author.roles] if author.roles else []
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
if ca:
user_roles = ca.role_list
else:
user_roles = []
logger.debug(f"[decorators] Роли пользователя {author_id}: {user_roles}")
# Проверяем наличие роли admin или editor

View File

@@ -11,6 +11,7 @@ from sqlalchemy.orm import exc
from auth.orm import Author
from auth.state import AuthState
from auth.tokens.storage import TokenStorage as TokenManager
from orm.community import CommunityAuthor
from services.db import local_session
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from utils.logger import root_logger as logger
@@ -48,7 +49,11 @@ async def verify_internal_auth(token: str) -> tuple[int, list, bool]:
author = session.query(Author).filter(Author.id == payload.user_id).one()
# Получаем роли
roles = [role.id for role in author.roles]
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
if ca:
roles = ca.role_list
else:
roles = []
logger.debug(f"[verify_internal_auth] Роли пользователя: {roles}")
# Определяем, является ли пользователь администратором

View File

@@ -17,6 +17,7 @@ from starlette.types import ASGIApp
from auth.credentials import AuthCredentials
from auth.orm import Author
from auth.tokens.storage import TokenStorage as TokenManager
from orm.community import CommunityAuthor
from services.db import local_session
from settings import (
ADMIN_EMAILS as ADMIN_EMAILS_LIST,
@@ -117,10 +118,14 @@ class AuthMiddleware:
), UnauthenticatedUser()
# Получаем разрешения из ролей
scopes = author.get_permissions()
scopes = await author.get_permissions()
# Получаем роли для пользователя
roles = [role.id for role in author.roles] if author.roles else []
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
if ca:
roles = ca.role_list
else:
roles = []
# Обновляем last_seen
author.last_seen = int(time.time())

View File

@@ -559,6 +559,9 @@ def _update_author_profile(author: Author, profile: dict) -> None:
def _create_new_oauth_user(provider: str, profile: dict, email: str, session: Any) -> Author:
"""Создает нового пользователя из OAuth профиля"""
from orm.community import Community, CommunityAuthor, CommunityFollower
from utils.logger import root_logger as logger
slug = generate_unique_slug(profile["name"] or f"{provider}_{profile.get('id', 'user')}")
author = Author(
@@ -576,4 +579,40 @@ def _create_new_oauth_user(provider: str, profile: dict, email: str, session: An
# Добавляем OAuth данные для нового пользователя
author.set_oauth_account(provider, profile["id"], email=profile.get("email"))
# Добавляем пользователя в основное сообщество с дефолтными ролями
target_community_id = 1 # Основное сообщество
# Получаем сообщество для назначения дефолтных ролей
community = session.query(Community).filter(Community.id == target_community_id).first()
if community:
# Инициализируем права сообщества если нужно
try:
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(community.initialize_role_permissions())
except Exception as e:
logger.warning(f"Не удалось инициализировать права сообщества {target_community_id}: {e}")
# Получаем дефолтные роли сообщества или используем стандартные
try:
default_roles = community.get_default_roles()
if not default_roles:
default_roles = ["reader", "author"]
except AttributeError:
default_roles = ["reader", "author"]
# Создаем CommunityAuthor с дефолтными ролями
community_author = CommunityAuthor(
community_id=target_community_id, author_id=author.id, roles=",".join(default_roles)
)
session.add(community_author)
logger.info(f"Создана запись CommunityAuthor для OAuth пользователя {author.id} с ролями: {default_roles}")
# Добавляем пользователя в подписчики сообщества
follower = CommunityFollower(community=target_community_id, follower=int(author.id))
session.add(follower)
logger.info(f"OAuth пользователь {author.id} добавлен в подписчики сообщества {target_community_id}")
return author

View File

@@ -1,8 +1,8 @@
import time
from typing import Dict, Set
from typing import Any, Dict, Optional
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from auth.identity import Password
from services.db import BaseModel as Base
@@ -32,7 +32,6 @@ class AuthorBookmark(Base):
{"extend_existing": True},
)
id = None # type: ignore
author = Column(ForeignKey("author.id"), primary_key=True)
shout = Column(ForeignKey("shout.id"), primary_key=True)
@@ -54,7 +53,6 @@ class AuthorRating(Base):
{"extend_existing": True},
)
id = None # type: ignore
rater = Column(ForeignKey("author.id"), primary_key=True)
author = Column(ForeignKey("author.id"), primary_key=True)
plus = Column(Boolean)
@@ -77,59 +75,13 @@ class AuthorFollower(Base):
Index("idx_author_follower_follower", "follower"),
{"extend_existing": True},
)
id = None # type: ignore
id = None # type: ignore[assignment]
follower = Column(ForeignKey("author.id"), primary_key=True)
author = Column(ForeignKey("author.id"), primary_key=True)
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
auto = Column(Boolean, nullable=False, default=False)
class RolePermission(Base):
"""Связь роли с разрешениями"""
__tablename__ = "role_permission"
__table_args__ = {"extend_existing": True}
id = None # type: ignore
role = Column(ForeignKey("role.id"), primary_key=True, index=True)
permission = Column(ForeignKey("permission.id"), primary_key=True, index=True)
class Permission(Base):
"""Модель разрешения в системе RBAC"""
__tablename__ = "permission"
__table_args__ = {"extend_existing": True}
id = Column(String, primary_key=True, unique=True, nullable=False, default=None)
resource = Column(String, nullable=False)
operation = Column(String, nullable=False)
class Role(Base):
"""Модель роли в системе RBAC"""
__tablename__ = "role"
__table_args__ = {"extend_existing": True}
id = Column(String, primary_key=True, unique=True, nullable=False, default=None)
name = Column(String, nullable=False)
permissions = relationship(Permission, secondary="role_permission", lazy="joined")
class AuthorRole(Base):
"""Связь автора с ролями"""
__tablename__ = "author_role"
__table_args__ = {"extend_existing": True}
id = None # type: ignore
community = Column(ForeignKey("community.id"), primary_key=True, index=True, default=1)
author = Column(ForeignKey("author.id"), primary_key=True, index=True)
role = Column(ForeignKey("role.id"), primary_key=True, index=True)
class Author(Base):
"""
Расширенная модель автора с функциями аутентификации и авторизации
@@ -171,12 +123,7 @@ class Author(Base):
last_seen = Column(Integer, nullable=False, default=lambda: int(time.time()))
deleted_at = Column(Integer, nullable=True)
# Связи с ролями
roles = relationship(Role, secondary="author_role", lazy="joined")
# search_vector = Column(
# TSVectorType("name", "slug", "bio", "about", regconfig="pg_catalog.russian")
# )
oid = Column(String, nullable=True)
# Список защищенных полей, которые видны только владельцу и администраторам
_protected_fields = ["email", "password", "provider_access_token", "provider_refresh_token"]
@@ -186,21 +133,6 @@ class Author(Base):
"""Проверяет, аутентифицирован ли пользователь"""
return self.id is not None
def get_permissions(self) -> Dict[str, Set[str]]:
"""Получает все разрешения пользователя"""
permissions: Dict[str, Set[str]] = {}
for role in self.roles:
for permission in role.permissions:
if permission.resource not in permissions:
permissions[permission.resource] = set()
permissions[permission.resource].add(permission.operation)
return permissions
def has_permission(self, resource: str, operation: str) -> bool:
"""Проверяет наличие разрешения у пользователя"""
permissions = self.get_permissions()
return resource in permissions and operation in permissions[resource]
def verify_password(self, password: str) -> bool:
"""Проверяет пароль пользователя"""
return Password.verify(password, str(self.password)) if self.password else False
@@ -237,36 +169,39 @@ class Author(Base):
"""
return str(self.slug or self.email or self.phone or "")
def dict(self, access: bool = False) -> Dict:
def dict(self, access: bool = False) -> Dict[str, Any]:
"""
Сериализует объект Author в словарь с учетом прав доступа.
Сериализует объект автора в словарь.
Args:
access (bool, optional): Флаг, указывающий, доступны ли защищенные поля
access: Если True, включает защищенные поля
Returns:
dict: Словарь с атрибутами Author, отфильтрованный по правам доступа
Dict: Словарь с данными автора
"""
# Получаем все атрибуты объекта
result = {c.name: getattr(self, c.name) for c in self.__table__.columns}
result: Dict[str, Any] = {
"id": self.id,
"name": self.name,
"slug": self.slug,
"bio": self.bio,
"about": self.about,
"pic": self.pic,
"links": self.links,
"created_at": self.created_at,
"updated_at": self.updated_at,
"last_seen": self.last_seen,
"deleted_at": self.deleted_at,
"email_verified": self.email_verified,
}
# Добавляем роли как список идентификаторов и названий
if hasattr(self, "roles"):
result["roles"] = []
for role in self.roles:
if isinstance(role, dict):
result["roles"].append(role.get("id"))
# скрываем защищенные поля
if not access:
for field in self._protected_fields:
if field in result:
result[field] = None
# Добавляем защищенные поля только если запрошен полный доступ
if access:
result.update({"email": self.email, "phone": self.phone, "oauth": self.oauth})
return result
@classmethod
def find_by_oauth(cls, provider: str, provider_id: str, session):
def find_by_oauth(cls, provider: str, provider_id: str, session: Session) -> Optional["Author"]:
"""
Находит автора по OAuth провайдеру и ID
@@ -282,29 +217,30 @@ class Author(Base):
authors = session.query(cls).filter(cls.oauth.isnot(None)).all()
for author in authors:
if author.oauth and provider in author.oauth:
if author.oauth[provider].get("id") == provider_id:
oauth_data = author.oauth[provider] # type: ignore[index]
if isinstance(oauth_data, dict) and oauth_data.get("id") == provider_id:
return author
return None
def set_oauth_account(self, provider: str, provider_id: str, email: str = None):
def set_oauth_account(self, provider: str, provider_id: str, email: Optional[str] = None) -> None:
"""
Устанавливает OAuth аккаунт для автора
Args:
provider (str): Имя OAuth провайдера (google, github и т.д.)
provider_id (str): ID пользователя у провайдера
email (str, optional): Email от провайдера
email (Optional[str]): Email от провайдера
"""
if not self.oauth:
self.oauth = {} # type: ignore[assignment]
oauth_data = {"id": provider_id}
oauth_data: Dict[str, str] = {"id": provider_id}
if email:
oauth_data["email"] = email
self.oauth[provider] = oauth_data # type: ignore[index]
def get_oauth_account(self, provider: str):
def get_oauth_account(self, provider: str) -> Optional[Dict[str, Any]]:
"""
Получает OAuth аккаунт провайдера
@@ -314,9 +250,12 @@ class Author(Base):
Returns:
dict или None: Данные OAuth аккаунта или None если не найден
"""
if not self.oauth:
oauth_data = getattr(self, "oauth", None)
if not oauth_data:
return None
return self.oauth.get(provider)
if isinstance(oauth_data, dict):
return oauth_data.get(provider)
return None
def remove_oauth_account(self, provider: str):
"""

View File

@@ -5,12 +5,10 @@
на основе его роли в этом сообществе.
"""
from typing import Union
from sqlalchemy.orm import Session
from auth.orm import Author, Permission, Role, RolePermission
from orm.community import Community, CommunityFollower, CommunityRole
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(",")
@@ -24,19 +22,8 @@ class ContextualPermissionCheck:
учитывая как глобальные роли пользователя, так и его роли внутри сообщества.
"""
# Маппинг из ролей сообщества в системные роли RBAC
COMMUNITY_ROLE_MAP = {
CommunityRole.READER: "community_reader",
CommunityRole.AUTHOR: "community_author",
CommunityRole.EXPERT: "community_expert",
CommunityRole.EDITOR: "community_editor",
}
# Обратное отображение для отображения системных ролей в роли сообщества
RBAC_TO_COMMUNITY_ROLE = {v: k for k, v in COMMUNITY_ROLE_MAP.items()}
@staticmethod
def check_community_permission(
async def check_community_permission(
session: Session, author_id: int, community_slug: str, resource: str, operation: str
) -> bool:
"""
@@ -56,9 +43,8 @@ class ContextualPermissionCheck:
author = session.query(Author).filter(Author.id == author_id).one_or_none()
if not author:
return False
# Если это администратор (по списку email) или у него есть глобальное разрешение
if author.has_permission(resource, operation) or author.email in ADMIN_EMAILS:
# Если это администратор (по списку email)
if author.email in ADMIN_EMAILS:
return True
# 2. Проверка разрешений в контексте сообщества
@@ -71,44 +57,13 @@ class ContextualPermissionCheck:
if community.created_by == author_id:
return True
# Получаем роли пользователя в этом сообществе
community_follower = (
session.query(CommunityFollower)
.filter(CommunityFollower.author == author_id, CommunityFollower.community == community.id)
.one_or_none()
)
if not community_follower or not community_follower.roles:
# Пользователь не является членом сообщества или у него нет ролей
return False
# Преобразуем роли сообщества в RBAC роли
rbac_roles = []
community_roles = community_follower.get_roles()
for role in community_roles:
if role in ContextualPermissionCheck.COMMUNITY_ROLE_MAP:
rbac_role_id = ContextualPermissionCheck.COMMUNITY_ROLE_MAP[role]
rbac_roles.append(rbac_role_id)
if not rbac_roles:
return False
# Проверяем наличие разрешения для этих ролей
permission_id = f"{resource}:{operation}"
# Запрос на проверку разрешений для указанных ролей
return (
session.query(RolePermission)
.join(Role, Role.id == RolePermission.role)
.join(Permission, Permission.id == RolePermission.permission)
.filter(Role.id.in_(rbac_roles), Permission.id == permission_id)
.first()
is not None
)
ca = CommunityAuthor.find_by_user_and_community(author_id, community.id, session)
return bool(await ca.has_permission(permission_id))
@staticmethod
def get_user_community_roles(session: Session, author_id: int, community_slug: str) -> list[CommunityRole]:
async def get_user_community_roles(session: Session, author_id: int, community_slug: str) -> list[str]:
"""
Получает список ролей пользователя в сообществе.
@@ -127,24 +82,13 @@ class ContextualPermissionCheck:
# Если автор является создателем сообщества, то у него есть роль владельца
if community.created_by == author_id:
return [CommunityRole.EDITOR] # Владелец имеет роль редактора по умолчанию
return ["editor", "author", "expert", "reader"]
# Получаем роли пользователя в этом сообществе
community_follower = (
session.query(CommunityFollower)
.filter(CommunityFollower.author == author_id, CommunityFollower.community == community.id)
.one_or_none()
)
if not community_follower or not community_follower.roles:
return []
return community_follower.get_roles()
ca = CommunityAuthor.find_by_user_and_community(author_id, community.id, session)
return ca.role_list if ca else []
@staticmethod
def assign_role_to_user(
session: Session, author_id: int, community_slug: str, role: Union[CommunityRole, str]
) -> bool:
async def assign_role_to_user(session: Session, author_id: int, community_slug: str, role: str) -> bool:
"""
Назначает роль пользователю в сообществе.
@@ -157,12 +101,6 @@ class ContextualPermissionCheck:
Returns:
bool: True если роль успешно назначена, иначе False
"""
# Преобразуем строковую роль в CommunityRole если нужно
if isinstance(role, str):
try:
role = CommunityRole(role)
except ValueError:
return False
# Получаем информацию о сообществе
community = session.query(Community).filter(Community.slug == community_slug).one_or_none()
@@ -170,30 +108,16 @@ class ContextualPermissionCheck:
return False
# Проверяем существование связи автор-сообщество
community_follower = (
session.query(CommunityFollower)
.filter(CommunityFollower.author == author_id, CommunityFollower.community == community.id)
.one_or_none()
)
if not community_follower:
# Создаем новую запись CommunityFollower
community_follower = CommunityFollower(follower=author_id, community=community.id)
session.add(community_follower)
ca = CommunityAuthor.find_by_user_and_community(author_id, community.id, session)
if not ca:
return False
# Назначаем роль
current_roles = community_follower.get_roles() if community_follower.roles else []
if role not in current_roles:
current_roles.append(role)
community_follower.set_roles(current_roles)
session.commit()
ca.add_role(role)
return True
@staticmethod
def revoke_role_from_user(
session: Session, author_id: int, community_slug: str, role: Union[CommunityRole, str]
) -> bool:
async def revoke_role_from_user(session: Session, author_id: int, community_slug: str, role: str) -> bool:
"""
Отзывает роль у пользователя в сообществе.
@@ -206,12 +130,6 @@ class ContextualPermissionCheck:
Returns:
bool: True если роль успешно отозвана, иначе False
"""
# Преобразуем строковую роль в CommunityRole если нужно
if isinstance(role, str):
try:
role = CommunityRole(role)
except ValueError:
return False
# Получаем информацию о сообществе
community = session.query(Community).filter(Community.slug == community_slug).one_or_none()
@@ -219,20 +137,10 @@ class ContextualPermissionCheck:
return False
# Проверяем существование связи автор-сообщество
community_follower = (
session.query(CommunityFollower)
.filter(CommunityFollower.author == author_id, CommunityFollower.community == community.id)
.one_or_none()
)
if not community_follower or not community_follower.roles:
ca = CommunityAuthor.find_by_user_and_community(author_id, community.id, session)
if not ca:
return False
# Отзываем роль
current_roles = community_follower.get_roles()
if role in current_roles:
current_roles.remove(role)
community_follower.set_roles(current_roles)
session.commit()
ca.remove_role(role)
return True