This commit is contained in:
@@ -1,403 +1,477 @@
|
|||||||
# Система RBAC (Role-Based Access Control)
|
# Система ролей и разрешений (RBAC)
|
||||||
|
|
||||||
## Обзор
|
## Общее описание
|
||||||
|
|
||||||
Система управления доступом на основе ролей для платформы Discours. Роли хранятся в CSV формате в таблице `CommunityAuthor` и могут быть назначены пользователям в рамках конкретного сообщества.
|
Система управления доступом на основе ролей (Role-Based Access Control, RBAC) обеспечивает гибкое управление правами пользователей в рамках сообществ платформы.
|
||||||
|
|
||||||
> **v0.6.11: Важно!** Наследование разрешений между ролями происходит **только при инициализации** прав для сообщества. В Redis хранятся уже развернутые (полные) списки разрешений для каждой роли. При запросе прав никакого on-the-fly наследования не происходит — только lookup по роли.
|
## Архитектура системы
|
||||||
|
|
||||||
## Архитектура
|
### Основные компоненты
|
||||||
|
|
||||||
### Основные принципы
|
1. **Community** - сообщество, контекст для ролей
|
||||||
- **CSV хранение**: Роли хранятся как CSV строка в поле `roles` таблицы `CommunityAuthor`
|
2. **CommunityAuthor** - связь пользователя с сообществом и его ролями
|
||||||
- **Простота**: Один пользователь может иметь несколько ролей в одном сообществе
|
3. **Role** - роль пользователя (reader, author, editor, admin)
|
||||||
- **Привязка к сообществу**: Роли существуют в контексте конкретного сообщества
|
4. **Permission** - разрешение на выполнение действия
|
||||||
- **Иерархия ролей**: `reader` → `author` → `artist` → `expert` → `editor` → `admin`
|
5. **RBAC Service** - сервис управления ролями и разрешениями
|
||||||
- **Наследование прав**: Каждая роль наследует все права предыдущих ролей **только при инициализации**
|
|
||||||
|
|
||||||
### Схема базы данных
|
### Модель данных
|
||||||
|
|
||||||
#### Таблица `community_author`
|
|
||||||
```sql
|
```sql
|
||||||
|
-- Основная таблица связи пользователя с сообществом
|
||||||
CREATE TABLE community_author (
|
CREATE TABLE community_author (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
community_id INTEGER REFERENCES community(id) NOT NULL,
|
community_id INTEGER REFERENCES community(id),
|
||||||
author_id INTEGER REFERENCES author(id) NOT NULL,
|
author_id INTEGER REFERENCES author(id),
|
||||||
roles TEXT, -- CSV строка ролей ("reader,author,expert")
|
roles TEXT, -- CSV строка ролей: "reader,author,editor"
|
||||||
joined_at INTEGER NOT NULL, -- Unix timestamp присоединения
|
joined_at INTEGER NOT NULL,
|
||||||
|
UNIQUE(community_id, author_id)
|
||||||
CONSTRAINT uq_community_author UNIQUE (community_id, author_id)
|
|
||||||
);
|
);
|
||||||
```
|
|
||||||
|
|
||||||
#### Индексы
|
-- Индексы для производительности
|
||||||
```sql
|
|
||||||
CREATE INDEX idx_community_author_community ON community_author(community_id);
|
CREATE INDEX idx_community_author_community ON community_author(community_id);
|
||||||
CREATE INDEX idx_community_author_author ON community_author(author_id);
|
CREATE INDEX idx_community_author_author ON community_author(author_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Работа с ролями
|
## Роли в системе
|
||||||
|
|
||||||
### Модель CommunityAuthor
|
### Базовые роли
|
||||||
|
|
||||||
#### Основные методы
|
#### 1. `reader` (Читатель)
|
||||||
```python
|
- **Обязательная роль для всех пользователей**
|
||||||
from orm.community import CommunityAuthor
|
- **Права:**
|
||||||
|
- Чтение публикаций
|
||||||
|
- Просмотр комментариев
|
||||||
|
- Подписка на сообщества
|
||||||
|
- Базовая навигация по платформе
|
||||||
|
|
||||||
# Получение списка ролей
|
#### 2. `author` (Автор)
|
||||||
ca = session.query(CommunityAuthor).first()
|
- **Права:**
|
||||||
roles = ca.role_list # ['reader', 'author', 'expert']
|
- Все права `reader`
|
||||||
|
- Создание публикаций (шаутов)
|
||||||
|
- Редактирование своих публикаций
|
||||||
|
- Комментирование
|
||||||
|
- Создание черновиков
|
||||||
|
|
||||||
# Установка ролей
|
#### 3. `artist` (Художник)
|
||||||
ca.role_list = ['reader', 'author']
|
- **Права:**
|
||||||
|
- Все права `author`
|
||||||
|
- Может быть указан как credited artist
|
||||||
|
- Загрузка и управление медиафайлами
|
||||||
|
|
||||||
# Проверка роли
|
#### 4. `expert` (Эксперт)
|
||||||
has_author = ca.has_role('author') # True
|
- **Права:**
|
||||||
|
- Все права `author`
|
||||||
|
- Добавление доказательств (evidence)
|
||||||
|
- Верификация контента
|
||||||
|
- Экспертная оценка публикаций
|
||||||
|
|
||||||
# Добавление роли
|
#### 5. `editor` (Редактор)
|
||||||
ca.add_role('expert')
|
- **Права:**
|
||||||
|
- Все права `expert`
|
||||||
|
- Модерация контента
|
||||||
|
- Редактирование чужих публикаций
|
||||||
|
- Управление тегами и категориями
|
||||||
|
- Модерация комментариев
|
||||||
|
|
||||||
# Удаление роли
|
#### 6. `admin` (Администратор)
|
||||||
ca.remove_role('author')
|
- **Права:**
|
||||||
|
- Все права `editor`
|
||||||
# Установка полного списка ролей
|
- Управление пользователями
|
||||||
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`
|
|
||||||
```python
|
|
||||||
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
|
admin > editor > expert > artist/author > reader
|
||||||
```
|
```
|
||||||
|
|
||||||
Каждая роль наследует все права предыдущих ролей в дефолтной иерархии **только при создании сообщества**.
|
Каждая роль автоматически включает права всех ролей ниже по иерархии.
|
||||||
|
|
||||||
### Стандартные роли и их права
|
## Разрешения (Permissions)
|
||||||
|
|
||||||
| Роль | Базовые права | Дополнительные права |
|
|
||||||
|------|---------------|---------------------|
|
|
||||||
| `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)
|
|
||||||
|
|
||||||
### Права на топики
|
Разрешения записываются в формате `resource:action`:
|
||||||
- `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
|
- `shout:create` - создание публикаций
|
||||||
|
- `shout:edit` - редактирование публикаций
|
||||||
|
- `shout:delete` - удаление публикаций
|
||||||
|
- `comment:create` - создание комментариев
|
||||||
|
- `comment:moderate` - модерация комментариев
|
||||||
|
- `user:manage` - управление пользователями
|
||||||
|
- `community:settings` - настройки сообщества
|
||||||
|
|
||||||
### Запросы
|
### Категории разрешений
|
||||||
|
|
||||||
#### Получение участников сообщества с ролями
|
#### Контент (Content)
|
||||||
```graphql
|
- `shout:create` - создание шаутов
|
||||||
query AdminGetCommunityMembers(
|
- `shout:edit_own` - редактирование своих шаутов
|
||||||
$community_id: Int!
|
- `shout:edit_any` - редактирование любых шаутов
|
||||||
$page: Int = 1
|
- `shout:delete_own` - удаление своих шаутов
|
||||||
$limit: Int = 50
|
- `shout:delete_any` - удаление любых шаутов
|
||||||
) {
|
- `shout:publish` - публикация шаутов
|
||||||
adminGetCommunityMembers(
|
- `shout:feature` - продвижение шаутов
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Мутации
|
#### Комментарии (Comments)
|
||||||
|
- `comment:create` - создание комментариев
|
||||||
|
- `comment:edit_own` - редактирование своих комментариев
|
||||||
|
- `comment:edit_any` - редактирование любых комментариев
|
||||||
|
- `comment:delete_own` - удаление своих комментариев
|
||||||
|
- `comment:delete_any` - удаление любых комментариев
|
||||||
|
- `comment:moderate` - модерация комментариев
|
||||||
|
|
||||||
#### Назначение ролей пользователю
|
#### Пользователи (Users)
|
||||||
```graphql
|
- `user:view_profile` - просмотр профилей
|
||||||
mutation AdminSetUserCommunityRoles(
|
- `user:edit_own_profile` - редактирование своего профиля
|
||||||
$author_id: Int!
|
- `user:manage_roles` - управление ролями пользователей
|
||||||
$community_id: Int!
|
- `user:ban` - блокировка пользователей
|
||||||
$roles: [String!]!
|
|
||||||
) {
|
|
||||||
adminSetUserCommunityRoles(
|
|
||||||
author_id: $author_id
|
|
||||||
community_id: $community_id
|
|
||||||
roles: $roles
|
|
||||||
) {
|
|
||||||
success
|
|
||||||
error
|
|
||||||
author_id
|
|
||||||
community_id
|
|
||||||
roles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Обновление настроек ролей сообщества
|
#### Сообщество (Community)
|
||||||
```graphql
|
- `community:view` - просмотр сообщества
|
||||||
mutation AdminUpdateCommunityRoleSettings(
|
- `community:settings` - настройки сообщества
|
||||||
$community_id: Int!
|
- `community:manage_members` - управление участниками
|
||||||
$default_roles: [String!]!
|
- `community:analytics` - просмотр аналитики
|
||||||
$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
|
## Логика работы системы
|
||||||
|
|
||||||
|
### 1. Регистрация пользователя
|
||||||
|
|
||||||
|
При регистрации пользователя:
|
||||||
|
|
||||||
### Импорт декораторов
|
|
||||||
```python
|
```python
|
||||||
from resolvers.rbac import (
|
# 1. Создается запись в Author
|
||||||
require_permission, require_role, admin_only,
|
user = Author(email=email, name=name, ...)
|
||||||
authenticated_only, require_any_permission,
|
|
||||||
require_all_permissions, RBACError
|
# 2. Создается связь с дефолтным сообществом (ID=1)
|
||||||
|
community_author = CommunityAuthor(
|
||||||
|
community_id=1,
|
||||||
|
author_id=user.id,
|
||||||
|
roles="reader,author" # Дефолтные роли
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Создается подписка на сообщество
|
||||||
|
follower = CommunityFollower(
|
||||||
|
community=1,
|
||||||
|
follower=user.id
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Примеры использования
|
### 2. Проверка авторизации
|
||||||
|
|
||||||
|
При входе в систему проверяется наличие роли `reader`:
|
||||||
|
|
||||||
#### Проверка конкретного разрешения
|
|
||||||
```python
|
```python
|
||||||
@mutation.field("createShout")
|
def login(email, password):
|
||||||
@require_permission("shout:create")
|
# 1. Найти пользователя
|
||||||
async def create_shout(self, info: GraphQLResolveInfo, **kwargs):
|
author = Author.get_by_email(email)
|
||||||
# Только пользователи с правом создания статей
|
|
||||||
return await self._create_shout_logic(**kwargs)
|
|
||||||
|
|
||||||
@mutation.field("create_topic")
|
# 2. Проверить пароль
|
||||||
@require_permission("topic:create")
|
if not verify_password(password, author.password):
|
||||||
async def create_topic(self, info: GraphQLResolveInfo, topic_input: dict):
|
return error("Неверный пароль")
|
||||||
# Только пользователи с правом создания топиков (author, editor)
|
|
||||||
return await self._create_topic_logic(topic_input)
|
|
||||||
|
|
||||||
@mutation.field("merge_topics")
|
# 3. Получить роли в дефолтном сообществе
|
||||||
@require_permission("topic:merge")
|
user_roles = get_user_roles_in_community(author.id, community_id=1)
|
||||||
async def merge_topics(self, info: GraphQLResolveInfo, merge_input: dict):
|
|
||||||
# Только пользователи с правом слияния топиков (editor)
|
# 4. Проверить наличие роли reader
|
||||||
return await self._merge_topics_logic(merge_input)
|
if "reader" not in user_roles and author.email not in ADMIN_EMAILS:
|
||||||
|
return error("Нет прав для входа. Требуется роль 'reader'.")
|
||||||
|
|
||||||
|
# 5. Создать сессию
|
||||||
|
return create_session(author)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Проверка любого из разрешений (OR логика)
|
### 3. Проверка разрешений
|
||||||
|
|
||||||
|
При выполнении действий проверяются разрешения:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@mutation.field("updateShout")
|
@login_required
|
||||||
@require_any_permission(["shout:update_own", "shout:update_any"])
|
async def create_shout(info, input):
|
||||||
async def update_shout(self, info: GraphQLResolveInfo, shout_id: int, **kwargs):
|
user_id = info.context["author"]["id"]
|
||||||
# Может редактировать свои статьи ИЛИ любые статьи
|
|
||||||
return await self._update_shout_logic(shout_id, **kwargs)
|
|
||||||
|
|
||||||
@mutation.field("update_topic")
|
# Проверяем разрешение на создание шаутов
|
||||||
@require_any_permission(["topic:update_own", "topic:update_any"])
|
has_permission = await check_user_permission_in_community(
|
||||||
async def update_topic(self, info: GraphQLResolveInfo, topic_input: dict):
|
user_id,
|
||||||
# Может редактировать свои топики ИЛИ любые топики
|
"shout:create",
|
||||||
return await self._update_topic_logic(topic_input)
|
community_id=1
|
||||||
|
)
|
||||||
|
|
||||||
@mutation.field("delete_topic")
|
if not has_permission:
|
||||||
@require_any_permission(["topic:delete_own", "topic:delete_any"])
|
raise GraphQLError("Недостаточно прав для создания публикации")
|
||||||
async def delete_topic(self, info: GraphQLResolveInfo, topic_id: int):
|
|
||||||
# Может удалять свои топики ИЛИ любые топики
|
# Создаем шаут
|
||||||
return await self._delete_topic_logic(topic_id)
|
return Shout.create(input)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Проверка конкретной роли
|
### 4. Управление ролями
|
||||||
|
|
||||||
|
#### Назначение ролей
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@mutation.field("verifyEvidence")
|
# Назначить роль пользователю
|
||||||
@require_role("expert")
|
assign_role_to_user(user_id=123, role="editor", community_id=1)
|
||||||
async def verify_evidence(self, info: GraphQLResolveInfo, **kwargs):
|
|
||||||
# Только эксперты могут верифицировать доказательства
|
# Убрать роль
|
||||||
return await self._verify_evidence_logic(**kwargs)
|
remove_role_from_user(user_id=123, role="editor", community_id=1)
|
||||||
|
|
||||||
|
# Установить все роли
|
||||||
|
community.set_user_roles(user_id=123, roles=["reader", "author", "editor"])
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Только для администраторов
|
#### Проверка ролей
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@mutation.field("deleteAnyContent")
|
# Получить роли пользователя
|
||||||
@admin_only
|
roles = get_user_roles_in_community(user_id=123, community_id=1)
|
||||||
async def delete_any_content(self, info: GraphQLResolveInfo, content_id: int):
|
|
||||||
# Только администраторы
|
# Проверить конкретную роль
|
||||||
return await self._delete_content_logic(content_id)
|
has_role = "editor" in roles
|
||||||
|
|
||||||
|
# Проверить разрешение
|
||||||
|
has_permission = await check_user_permission_in_community(
|
||||||
|
user_id=123,
|
||||||
|
permission="shout:edit_any",
|
||||||
|
community_id=1
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Обработка ошибок
|
## Конфигурация сообщества
|
||||||
|
|
||||||
|
### Дефолтные роли
|
||||||
|
|
||||||
|
Каждое сообщество может настроить свои дефолтные роли для новых пользователей:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from resolvers.rbac import RBACError
|
# Получить дефолтные роли
|
||||||
|
default_roles = community.get_default_roles() # ["reader", "author"]
|
||||||
|
|
||||||
try:
|
# Установить дефолтные роли
|
||||||
result = await some_rbac_protected_function()
|
community.set_default_roles(["reader"]) # Только reader по умолчанию
|
||||||
except RBACError as e:
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Настройка сообщества
|
### Доступные роли
|
||||||
|
|
||||||
|
Сообщество может ограничить список доступных ролей:
|
||||||
|
|
||||||
### Управление ролями в сообществе
|
|
||||||
```python
|
```python
|
||||||
from orm.community import Community
|
# Все роли доступны по умолчанию
|
||||||
|
available_roles = ["reader", "author", "artist", "expert", "editor", "admin"]
|
||||||
|
|
||||||
community = session.query(Community).filter(Community.id == 1).first()
|
# Ограничить только базовыми ролями
|
||||||
|
community.set_available_roles(["reader", "author", "editor"])
|
||||||
# Установка доступных ролей
|
|
||||||
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 контекстом
|
### Проблемы существующих пользователей
|
||||||
|
|
||||||
|
1. **Пользователи без роли `reader`** - не могут войти в систему
|
||||||
|
2. **Старая система ролей** - данные в `Author.roles` устарели
|
||||||
|
3. **Отсутствие связей `CommunityAuthor`** - новые пользователи без ролей
|
||||||
|
|
||||||
|
### Решения
|
||||||
|
|
||||||
|
#### 1. Автоматическое добавление роли `reader`
|
||||||
|
|
||||||
### Middleware для установки ролей
|
|
||||||
```python
|
```python
|
||||||
async def rbac_middleware(request, call_next):
|
async def ensure_user_has_reader_role(user_id: int) -> bool:
|
||||||
# Получаем автора из контекста
|
"""Убеждается, что у пользователя есть роль 'reader'"""
|
||||||
author = getattr(request.state, 'author', None)
|
existing_roles = get_user_roles_in_community(user_id, community_id=1)
|
||||||
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)
|
if "reader" not in existing_roles:
|
||||||
return response
|
success = assign_role_to_user(user_id, "reader", community_id=1)
|
||||||
|
if success:
|
||||||
|
logger.info(f"Роль 'reader' добавлена пользователю {user_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return True
|
||||||
```
|
```
|
||||||
|
|
||||||
### Получение ролей в resolver'ах
|
#### 2. Массовое исправление ролей
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def get_user_roles_from_context(info):
|
async def fix_all_users_reader_role() -> dict[str, int]:
|
||||||
"""Получение ролей пользователя из GraphQL контекста"""
|
"""Проверяет всех пользователей и добавляет роль 'reader'"""
|
||||||
# Из middleware
|
stats = {"checked": 0, "fixed": 0, "errors": 0}
|
||||||
user_roles = getattr(info.context, "user_roles", [])
|
|
||||||
if user_roles:
|
|
||||||
return user_roles
|
|
||||||
|
|
||||||
# Из author'а напрямую
|
all_authors = session.query(Author).all()
|
||||||
author = getattr(info.context, "author", None)
|
|
||||||
if author and hasattr(author, "roles"):
|
|
||||||
return author.roles.split(",") if author.roles else []
|
|
||||||
|
|
||||||
return []
|
for author in all_authors:
|
||||||
|
stats["checked"] += 1
|
||||||
|
try:
|
||||||
|
await ensure_user_has_reader_role(author.id)
|
||||||
|
stats["fixed"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка для пользователя {author.id}: {e}")
|
||||||
|
stats["errors"] += 1
|
||||||
|
|
||||||
|
return stats
|
||||||
```
|
```
|
||||||
|
|
||||||
## Миграция и обновления
|
#### 3. Миграция из старой системы
|
||||||
|
|
||||||
### Миграция с предыдущей системы ролей
|
```python
|
||||||
Если в проекте была отдельная таблица ролей, необходимо:
|
def migrate_old_roles_to_community_author():
|
||||||
|
"""Переносит роли из старой системы в CommunityAuthor"""
|
||||||
|
|
||||||
1. Создать миграцию для добавления поля `roles` в `CommunityAuthor`
|
# Получаем все старые роли из Author.roles
|
||||||
2. Перенести данные из старых таблиц в CSV формат
|
old_roles = session.query(AuthorRole).all()
|
||||||
3. Удалить старые таблицы ролей
|
|
||||||
|
|
||||||
```bash
|
for role in old_roles:
|
||||||
alembic revision --autogenerate -m "Add CSV roles to CommunityAuthor"
|
# Создаем запись CommunityAuthor
|
||||||
alembic upgrade head
|
ca = CommunityAuthor(
|
||||||
|
community_id=role.community,
|
||||||
|
author_id=role.author,
|
||||||
|
roles=role.role
|
||||||
|
)
|
||||||
|
session.add(ca)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Обновление CHANGELOG.md
|
## API для работы с ролями
|
||||||
После внесения изменений в RBAC систему обновляется `CHANGELOG.md` с новой версией.
|
|
||||||
|
### GraphQL мутации
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# Назначить роль пользователю
|
||||||
|
mutation AssignRole($userId: Int!, $role: String!, $communityId: Int) {
|
||||||
|
assignRole(userId: $userId, role: $role, communityId: $communityId) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Убрать роль
|
||||||
|
mutation RemoveRole($userId: Int!, $role: String!, $communityId: Int) {
|
||||||
|
removeRole(userId: $userId, role: $role, communityId: $communityId) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Установить все роли пользователя
|
||||||
|
mutation SetUserRoles($userId: Int!, $roles: [String!]!, $communityId: Int) {
|
||||||
|
setUserRoles(userId: $userId, roles: $roles, communityId: $communityId) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GraphQL запросы
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
# Получить роли пользователя
|
||||||
|
query GetUserRoles($userId: Int!, $communityId: Int) {
|
||||||
|
userRoles(userId: $userId, communityId: $communityId) {
|
||||||
|
roles
|
||||||
|
permissions
|
||||||
|
community {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Получить всех участников сообщества с ролями
|
||||||
|
query GetCommunityMembers($communityId: Int!) {
|
||||||
|
communityMembers(communityId: $communityId) {
|
||||||
|
authorId
|
||||||
|
roles
|
||||||
|
permissions
|
||||||
|
joinedAt
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Принципы безопасности
|
||||||
|
|
||||||
|
1. **Принцип минимальных привилегий** - пользователь получает только необходимые права
|
||||||
|
2. **Разделение обязанностей** - разные роли для разных функций
|
||||||
|
3. **Аудит действий** - логирование всех изменений ролей
|
||||||
|
4. **Проверка на каждом уровне** - валидация разрешений в API и UI
|
||||||
|
|
||||||
|
### Защита от атак
|
||||||
|
|
||||||
|
1. **Privilege Escalation** - проверка прав на изменение ролей
|
||||||
|
2. **Mass Assignment** - валидация входных данных
|
||||||
|
3. **CSRF** - использование токенов для изменения ролей
|
||||||
|
4. **XSS** - экранирование данных ролей в UI
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Логирование изменений ролей
|
||||||
|
logger.info(f"Role {role} assigned to user {user_id} by admin {admin_id}")
|
||||||
|
logger.warning(f"Failed login attempt for user without reader role: {user_id}")
|
||||||
|
logger.error(f"Permission denied: user {user_id} tried to access {resource}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Тестовые сценарии
|
||||||
|
|
||||||
|
1. **Регистрация пользователя** - проверка назначения дефолтных ролей
|
||||||
|
2. **Вход в систему** - проверка требования роли `reader`
|
||||||
|
3. **Назначение ролей** - проверка прав администратора
|
||||||
|
4. **Проверка разрешений** - валидация доступа к ресурсам
|
||||||
|
5. **Иерархия ролей** - наследование прав
|
||||||
|
|
||||||
|
### Пример тестов
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_user_registration_assigns_default_roles():
|
||||||
|
"""Проверяет назначение дефолтных ролей при регистрации"""
|
||||||
|
user = create_user(email="test@test.com")
|
||||||
|
roles = get_user_roles_in_community(user.id, community_id=1)
|
||||||
|
|
||||||
|
assert "reader" in roles
|
||||||
|
assert "author" in roles
|
||||||
|
|
||||||
|
def test_login_requires_reader_role():
|
||||||
|
"""Проверяет требование роли reader для входа"""
|
||||||
|
user = create_user_without_roles(email="test@test.com")
|
||||||
|
|
||||||
|
result = login(email="test@test.com", password="password")
|
||||||
|
|
||||||
|
assert result["success"] == False
|
||||||
|
assert "reader" in result["error"]
|
||||||
|
|
||||||
|
def test_role_hierarchy():
|
||||||
|
"""Проверяет иерархию ролей"""
|
||||||
|
user = create_user(email="admin@test.com")
|
||||||
|
assign_role_to_user(user.id, "admin", community_id=1)
|
||||||
|
|
||||||
|
# Админ должен иметь все права
|
||||||
|
assert check_permission(user.id, "shout:create")
|
||||||
|
assert check_permission(user.id, "user:manage")
|
||||||
|
assert check_permission(user.id, "community:settings")
|
||||||
|
```
|
||||||
|
|
||||||
## Производительность
|
## Производительность
|
||||||
|
|
||||||
### Оптимизация
|
### Оптимизации
|
||||||
- CSV роли хранятся в одном поле, что снижает количество JOIN'ов
|
|
||||||
- Индексы на `community_id` и `author_id` ускоряют запросы
|
|
||||||
- Кеширование разрешений на уровне приложения
|
|
||||||
|
|
||||||
### Рекомендации
|
1. **Кеширование ролей** - хранение ролей пользователя в Redis
|
||||||
- Избегать частых изменений ролей
|
2. **Индексы БД** - быстрый поиск по `community_id` и `author_id`
|
||||||
- Кешировать результаты `get_role_permissions_for_community()`
|
3. **Batch операции** - массовое назначение ролей
|
||||||
- Использовать bulk операции для массового назначения ролей
|
4. **Ленивая загрузка** - загрузка разрешений по требованию
|
||||||
|
|
||||||
|
### Мониторинг
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Метрики для Prometheus
|
||||||
|
role_checks_total = Counter('rbac_role_checks_total')
|
||||||
|
permission_checks_total = Counter('rbac_permission_checks_total')
|
||||||
|
role_assignments_total = Counter('rbac_role_assignments_total')
|
||||||
|
```
|
||||||
|
157
services/auth.py
157
services/auth.py
@@ -9,7 +9,6 @@ import time
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from sqlalchemy import exc
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from auth.email import send_auth_email
|
from auth.email import send_auth_email
|
||||||
@@ -80,24 +79,39 @@ class AuthService:
|
|||||||
f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}"
|
f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Если в ролях нет админа, но есть ID - проверяем в БД
|
# Если в ролях нет админа, но есть ID - проверяем через новую систему RBAC
|
||||||
if user_id and not is_admin:
|
if user_id and not is_admin:
|
||||||
try:
|
try:
|
||||||
with local_session() as session:
|
# Преобразуем user_id в число
|
||||||
# Преобразуем user_id в число
|
try:
|
||||||
try:
|
if isinstance(user_id, str):
|
||||||
if isinstance(user_id, str):
|
user_id_int = int(user_id.strip())
|
||||||
user_id_int = int(user_id.strip())
|
|
||||||
else:
|
|
||||||
user_id_int = int(user_id)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
|
||||||
else:
|
else:
|
||||||
# Проверяем наличие админских прав через новую RBAC систему
|
user_id_int = int(user_id)
|
||||||
from orm.community import get_user_roles_in_community
|
except (ValueError, TypeError):
|
||||||
|
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
||||||
|
return 0, [], False
|
||||||
|
|
||||||
|
# Получаем роли через новую систему CommunityAuthor
|
||||||
|
from orm.community import get_user_roles_in_community
|
||||||
|
|
||||||
|
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
|
||||||
|
logger.debug(f"[check_auth] Роли из CommunityAuthor: {user_roles_in_community}")
|
||||||
|
|
||||||
|
# Обновляем роли из новой системы
|
||||||
|
user_roles = user_roles_in_community
|
||||||
|
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
|
||||||
|
|
||||||
|
# Проверяем админские права через email если нет роли админа
|
||||||
|
if not is_admin:
|
||||||
|
with local_session() as session:
|
||||||
|
author = session.query(Author).filter(Author.id == user_id_int).first()
|
||||||
|
if author and author.email in ADMIN_EMAILS.split(","):
|
||||||
|
is_admin = True
|
||||||
|
logger.debug(
|
||||||
|
f"[check_auth] Пользователь {author.email} определен как админ через ADMIN_EMAILS"
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
||||||
|
|
||||||
@@ -112,26 +126,30 @@ class AuthService:
|
|||||||
|
|
||||||
logger.info(f"Adding roles {roles} to user {user_id}")
|
logger.info(f"Adding roles {roles} to user {user_id}")
|
||||||
|
|
||||||
from orm.community import assign_role_to_user
|
try:
|
||||||
|
user_id_int = int(user_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
||||||
|
return None
|
||||||
|
|
||||||
logger.debug("Using local authentication with new RBAC system")
|
from orm.community import assign_role_to_user, get_user_roles_in_community
|
||||||
with local_session() as session:
|
|
||||||
try:
|
|
||||||
author = session.query(Author).filter(Author.id == user_id).one()
|
|
||||||
|
|
||||||
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
|
# Проверяем существующие роли
|
||||||
for role_name in roles:
|
existing_roles = get_user_roles_in_community(user_id_int, community_id=1)
|
||||||
success = assign_role_to_user(int(user_id), role_name, community_id=1)
|
logger.debug(f"Существующие роли пользователя {user_id}: {existing_roles}")
|
||||||
if success:
|
|
||||||
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
|
|
||||||
|
|
||||||
return user_id
|
# Добавляем новые роли через новую систему RBAC
|
||||||
|
for role_name in roles:
|
||||||
|
if role_name not in existing_roles:
|
||||||
|
success = assign_role_to_user(user_id_int, role_name, community_id=1)
|
||||||
|
if success:
|
||||||
|
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Роль {role_name} уже есть у пользователя {user_id}")
|
||||||
|
|
||||||
except exc.NoResultFound:
|
return user_id
|
||||||
logger.error(f"Author {user_id} not found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
|
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
|
||||||
"""Создает нового пользователя с дефолтными ролями"""
|
"""Создает нового пользователя с дефолтными ролями"""
|
||||||
@@ -332,17 +350,22 @@ class AuthService:
|
|||||||
logger.warning(f"Пользователь {email} не найден")
|
logger.warning(f"Пользователь {email} не найден")
|
||||||
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
||||||
|
|
||||||
# Проверяем роли (упрощенная версия)
|
# Проверяем роли через новую систему CommunityAuthor
|
||||||
has_reader_role = False
|
from orm.community import get_user_roles_in_community
|
||||||
if hasattr(author, "roles") and author.roles:
|
|
||||||
for role in author.roles:
|
user_roles = get_user_roles_in_community(author.id, community_id=1)
|
||||||
if role.id == "reader":
|
has_reader_role = "reader" in user_roles
|
||||||
has_reader_role = True
|
|
||||||
break
|
logger.debug(f"Роли пользователя {email}: {user_roles}")
|
||||||
|
|
||||||
if not has_reader_role and author.email not in ADMIN_EMAILS.split(","):
|
if not has_reader_role and author.email not in ADMIN_EMAILS.split(","):
|
||||||
logger.warning(f"У пользователя {email} нет роли 'reader'")
|
logger.warning(f"У пользователя {email} нет роли 'reader'. Текущие роли: {user_roles}")
|
||||||
return {"success": False, "token": None, "author": None, "error": "Нет прав для входа"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"token": None,
|
||||||
|
"author": None,
|
||||||
|
"error": "Нет прав для входа. Требуется роль 'reader'.",
|
||||||
|
}
|
||||||
|
|
||||||
# Проверяем пароль
|
# Проверяем пароль
|
||||||
try:
|
try:
|
||||||
@@ -610,6 +633,60 @@ class AuthService:
|
|||||||
logger.error(f"Ошибка отмены смены email: {e}")
|
logger.error(f"Ошибка отмены смены email: {e}")
|
||||||
return {"success": False, "error": str(e), "author": None}
|
return {"success": False, "error": str(e), "author": None}
|
||||||
|
|
||||||
|
async def ensure_user_has_reader_role(self, user_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Убеждается, что у пользователя есть роль 'reader'.
|
||||||
|
Если её нет - добавляет автоматически.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если роль была добавлена или уже существует
|
||||||
|
"""
|
||||||
|
from orm.community import assign_role_to_user, get_user_roles_in_community
|
||||||
|
|
||||||
|
existing_roles = get_user_roles_in_community(user_id, community_id=1)
|
||||||
|
|
||||||
|
if "reader" not in existing_roles:
|
||||||
|
logger.warning(f"У пользователя {user_id} нет роли 'reader'. Добавляем автоматически.")
|
||||||
|
success = assign_role_to_user(user_id, "reader", community_id=1)
|
||||||
|
if success:
|
||||||
|
logger.info(f"Роль 'reader' добавлена пользователю {user_id}")
|
||||||
|
return True
|
||||||
|
logger.error(f"Не удалось добавить роль 'reader' пользователю {user_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def fix_all_users_reader_role(self) -> dict[str, int]:
|
||||||
|
"""
|
||||||
|
Проверяет всех пользователей и добавляет роль 'reader' тем, у кого её нет.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Статистика операции: {"checked": int, "fixed": int, "errors": int}
|
||||||
|
"""
|
||||||
|
stats = {"checked": 0, "fixed": 0, "errors": 0}
|
||||||
|
|
||||||
|
with local_session() as session:
|
||||||
|
# Получаем всех пользователей
|
||||||
|
all_authors = session.query(Author).all()
|
||||||
|
|
||||||
|
for author in all_authors:
|
||||||
|
stats["checked"] += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
had_reader = await self.ensure_user_has_reader_role(author.id)
|
||||||
|
if not had_reader:
|
||||||
|
stats["fixed"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при исправлении ролей для пользователя {author.id}: {e}")
|
||||||
|
stats["errors"] += 1
|
||||||
|
|
||||||
|
logger.info(f"Исправление ролей завершено: {stats}")
|
||||||
|
return stats
|
||||||
|
|
||||||
def login_required(self, f: Callable) -> Callable:
|
def login_required(self, f: Callable) -> Callable:
|
||||||
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
|
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user