15 KiB
Система RBAC (Role-Based Access Control)
Обзор
Система управления доступом на основе ролей для платформы Discours. Роли хранятся в CSV формате в таблице CommunityAuthor
и могут быть назначены пользователям в рамках конкретного сообщества.
v0.6.11: Важно! Наследование разрешений между ролями происходит только при инициализации прав для сообщества. В Redis хранятся уже развернутые (полные) списки разрешений для каждой роли. При запросе прав никакого on-the-fly наследования не происходит — только lookup по роли.
Архитектура
Основные принципы
- CSV хранение: Роли хранятся как CSV строка в поле
roles
таблицыCommunityAuthor
- Простота: Один пользователь может иметь несколько ролей в одном сообществе
- Привязка к сообществу: Роли существуют в контексте конкретного сообщества
- Иерархия ролей:
reader
→author
→artist
→expert
→editor
→admin
- Наследование прав: Каждая роль наследует все права предыдущих ролей только при инициализации
Схема базы данных
Таблица 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 []
Миграция и обновления
Миграция с предыдущей системы ролей
Если в проекте была отдельная таблица ролей, необходимо:
- Создать миграцию для добавления поля
roles
вCommunityAuthor
- Перенести данные из старых таблиц в CSV формат
- Удалить старые таблицы ролей
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 операции для массового назначения ролей