core/docs/rbac-system.md
Untone eb2140bcc6
All checks were successful
Deploy on push / deploy (push) Successful in 6s
0.7.7-topics-editing
2025-07-03 12:15:10 +03:00

15 KiB
Raw Blame History

Система RBAC (Role-Based Access Control)

Обзор

Система управления доступом на основе ролей для платформы Discours. Роли хранятся в CSV формате в таблице CommunityAuthor и могут быть назначены пользователям в рамках конкретного сообщества.

v0.6.11: Важно! Наследование разрешений между ролями происходит только при инициализации прав для сообщества. В Redis хранятся уже развернутые (полные) списки разрешений для каждой роли. При запросе прав никакого on-the-fly наследования не происходит — только lookup по роли.

Архитектура

Основные принципы

  • CSV хранение: Роли хранятся как CSV строка в поле roles таблицы CommunityAuthor
  • Простота: Один пользователь может иметь несколько ролей в одном сообществе
  • Привязка к сообществу: Роли существуют в контексте конкретного сообщества
  • Иерархия ролей: readerauthorartistexperteditoradmin
  • Наследование прав: Каждая роль наследует все права предыдущих ролей только при инициализации

Схема базы данных

Таблица community_author

CREATE TABLE community_author (
    id INTEGER PRIMARY KEY,
    community_id INTEGER REFERENCES community(id) NOT NULL,
    author_id INTEGER REFERENCES author(id) NOT NULL,
    roles TEXT,                         -- CSV строка ролей ("reader,author,expert")
    joined_at INTEGER NOT NULL,         -- Unix timestamp присоединения

    CONSTRAINT uq_community_author UNIQUE (community_id, author_id)
);

Индексы

CREATE INDEX idx_community_author_community ON community_author(community_id);
CREATE INDEX idx_community_author_author ON community_author(author_id);

Работа с ролями

Модель CommunityAuthor

Основные методы

from orm.community import CommunityAuthor

# Получение списка ролей
ca = session.query(CommunityAuthor).first()
roles = ca.role_list  # ['reader', 'author', 'expert']

# Установка ролей
ca.role_list = ['reader', 'author']

# Проверка роли
has_author = ca.has_role('author')  # True

# Добавление роли
ca.add_role('expert')

# Удаление роли
ca.remove_role('author')

# Установка полного списка ролей
ca.set_roles(['reader', 'editor'])

# Получение всех разрешений
permissions = await ca.get_permissions()  # ['shout:read', 'shout:create', ...]

# Проверка разрешения
can_create = await ca.has_permission('shout:create')  # True

Вспомогательные функции

Основные функции из orm/community.py

from orm.community import (
    get_user_roles_in_community,
    check_user_permission_in_community,
    assign_role_to_user,
    remove_role_from_user,
    get_all_community_members_with_roles,
    bulk_assign_roles
)

# Получение ролей пользователя
roles = get_user_roles_in_community(author_id=123, community_id=1)
# Возвращает: ['reader', 'author']

# Проверка разрешения
has_perm = await check_user_permission_in_community(
    author_id=123,
    permission='shout:create',
    community_id=1
)

# Назначение роли
success = assign_role_to_user(
    author_id=123,
    role='expert',
    community_id=1
)

# Удаление роли
success = remove_role_from_user(
    author_id=123,
    role='author',
    community_id=1
)

# Получение всех участников с ролями
members = get_all_community_members_with_roles(community_id=1)
# Возвращает: [{'author_id': 123, 'roles': ['reader', 'author'], ...}, ...]

# Массовое назначение ролей
bulk_assign_roles([
    {'author_id': 123, 'roles': ['reader', 'author']},
    {'author_id': 456, 'roles': ['expert', 'editor']}
], community_id=1)

Система разрешений

Иерархия ролей

reader → author → artist → expert → editor → admin

Каждая роль наследует все права предыдущих ролей в дефолтной иерархии только при создании сообщества.

Стандартные роли и их права

Роль Базовые права Дополнительные права
reader *:read, базовые реакции chat:*, message:*, bookmark:*
author Наследует reader + *:create, *:update_own, *:delete_own draft:*
artist Наследует author reaction:CREDIT:accept, reaction:CREDIT:decline
expert Наследует author reaction:PROOF:*, reaction:DISPROOF:*, reaction:AGREE:*, reaction:DISAGREE:*
editor *:read, *:create, *:update_any, *:delete_any community:read, community:update_own, topic:merge, topic:create, topic:update_own, topic:delete_own
admin Все права (*) Полный доступ ко всем функциям

Формат разрешений

  • Базовые: <entity>:<action> (например: shout:create, topic:create)
  • Реакции: reaction:<type>:<action> (например: reaction:LIKE:create)
  • Специальные: topic:merge (слияние топиков)
  • Wildcard: <entity>:* или * (только для admin)

Права на топики

  • topic:create - создание новых топиков (роли: author, editor)
  • topic:read - чтение топиков (роли: reader и выше)
  • topic:update_own - обновление собственных топиков (роли: author)
  • topic:update_any - обновление любых топиков (роли: editor)
  • topic:delete_own - удаление собственных топиков (роли: author)
  • topic:delete_any - удаление любых топиков (роли: editor)
  • topic:merge - слияние топиков (роли: editor)

GraphQL API

Запросы

Получение участников сообщества с ролями

query AdminGetCommunityMembers(
    $community_id: Int!
    $page: Int = 1
    $limit: Int = 50
) {
    adminGetCommunityMembers(
        community_id: $community_id
        page: $page
        limit: $limit
    ) {
        success
        error
        members {
            id
            name
            slug
            email
            roles
            is_follower
            created_at
        }
        total
        page
        limit
        has_next
    }
}

Мутации

Назначение ролей пользователю

mutation AdminSetUserCommunityRoles(
    $author_id: Int!
    $community_id: Int!
    $roles: [String!]!
) {
    adminSetUserCommunityRoles(
        author_id: $author_id
        community_id: $community_id
        roles: $roles
    ) {
        success
        error
        author_id
        community_id
        roles
    }
}

Обновление настроек ролей сообщества

mutation AdminUpdateCommunityRoleSettings(
    $community_id: Int!
    $default_roles: [String!]!
    $available_roles: [String!]!
) {
    adminUpdateCommunityRoleSettings(
        community_id: $community_id
        default_roles: $default_roles
        available_roles: $available_roles
    ) {
        success
        error
        community_id
        default_roles
        available_roles
    }
}

Использование декораторов RBAC

Импорт декораторов

from resolvers.rbac import (
    require_permission, require_role, admin_only,
    authenticated_only, require_any_permission,
    require_all_permissions, RBACError
)

Примеры использования

Проверка конкретного разрешения

@mutation.field("createShout")
@require_permission("shout:create")
async def create_shout(self, info: GraphQLResolveInfo, **kwargs):
    # Только пользователи с правом создания статей
    return await self._create_shout_logic(**kwargs)

@mutation.field("create_topic")
@require_permission("topic:create")
async def create_topic(self, info: GraphQLResolveInfo, topic_input: dict):
    # Только пользователи с правом создания топиков (author, editor)
    return await self._create_topic_logic(topic_input)

@mutation.field("merge_topics")
@require_permission("topic:merge")
async def merge_topics(self, info: GraphQLResolveInfo, merge_input: dict):
    # Только пользователи с правом слияния топиков (editor)
    return await self._merge_topics_logic(merge_input)

Проверка любого из разрешений (OR логика)

@mutation.field("updateShout")
@require_any_permission(["shout:update_own", "shout:update_any"])
async def update_shout(self, info: GraphQLResolveInfo, shout_id: int, **kwargs):
    # Может редактировать свои статьи ИЛИ любые статьи
    return await self._update_shout_logic(shout_id, **kwargs)

@mutation.field("update_topic")
@require_any_permission(["topic:update_own", "topic:update_any"])
async def update_topic(self, info: GraphQLResolveInfo, topic_input: dict):
    # Может редактировать свои топики ИЛИ любые топики
    return await self._update_topic_logic(topic_input)

@mutation.field("delete_topic")
@require_any_permission(["topic:delete_own", "topic:delete_any"])
async def delete_topic(self, info: GraphQLResolveInfo, topic_id: int):
    # Может удалять свои топики ИЛИ любые топики
    return await self._delete_topic_logic(topic_id)

Проверка конкретной роли

@mutation.field("verifyEvidence")
@require_role("expert")
async def verify_evidence(self, info: GraphQLResolveInfo, **kwargs):
    # Только эксперты могут верифицировать доказательства
    return await self._verify_evidence_logic(**kwargs)

Только для администраторов

@mutation.field("deleteAnyContent")
@admin_only
async def delete_any_content(self, info: GraphQLResolveInfo, content_id: int):
    # Только администраторы
    return await self._delete_content_logic(content_id)

Обработка ошибок

from resolvers.rbac import RBACError

try:
    result = await some_rbac_protected_function()
except RBACError as e:
    return {"success": False, "error": str(e)}

Настройка сообщества

Управление ролями в сообществе

from orm.community import Community

community = session.query(Community).filter(Community.id == 1).first()

# Установка доступных ролей
community.set_available_roles(['reader', 'author', 'expert', 'admin'])

# Установка дефолтных ролей для новых участников
community.set_default_roles(['reader'])

# Получение настроек
available = community.get_available_roles()  # ['reader', 'author', 'expert', 'admin']
default = community.get_default_roles()      # ['reader']

Автоматическое назначение дефолтных ролей

При создании связи пользователя с сообществом автоматически назначаются роли из default_roles.

Интеграция с GraphQL контекстом

Middleware для установки ролей

async def rbac_middleware(request, call_next):
    # Получаем автора из контекста
    author = getattr(request.state, 'author', None)
    if author:
        # Устанавливаем роли в контекст для текущего сообщества
        community_id = get_current_community_id(request)
        if community_id:
            user_roles = get_user_roles_in_community(author.id, community_id)
            request.state.user_roles = user_roles

    response = await call_next(request)
    return response

Получение ролей в resolver'ах

def get_user_roles_from_context(info):
    """Получение ролей пользователя из GraphQL контекста"""
    # Из middleware
    user_roles = getattr(info.context, "user_roles", [])
    if user_roles:
        return user_roles

    # Из author'а напрямую
    author = getattr(info.context, "author", None)
    if author and hasattr(author, "roles"):
        return author.roles.split(",") if author.roles else []

    return []

Миграция и обновления

Миграция с предыдущей системы ролей

Если в проекте была отдельная таблица ролей, необходимо:

  1. Создать миграцию для добавления поля roles в CommunityAuthor
  2. Перенести данные из старых таблиц в CSV формат
  3. Удалить старые таблицы ролей
alembic revision --autogenerate -m "Add CSV roles to CommunityAuthor"
alembic upgrade head

Обновление CHANGELOG.md

После внесения изменений в RBAC систему обновляется CHANGELOG.md с новой версией.

Производительность

Оптимизация

  • CSV роли хранятся в одном поле, что снижает количество JOIN'ов
  • Индексы на community_id и author_id ускоряют запросы
  • Кеширование разрешений на уровне приложения

Рекомендации

  • Избегать частых изменений ролей
  • Кешировать результаты get_role_permissions_for_community()
  • Использовать bulk операции для массового назначения ролей