auth-fix
All checks were successful
Deploy on push / deploy (push) Successful in 13s

This commit is contained in:
2025-07-23 13:29:49 +03:00
parent 3826797317
commit 867232e48f
2 changed files with 504 additions and 353 deletions

View File

@@ -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
from resolvers.rbac import RBACError
### Дефолтные роли
Каждое сообщество может настроить свои дефолтные роли для новых пользователей:
```python
# Получить дефолтные роли
default_roles = community.get_default_roles() # ["reader", "author"]
# Установить дефолтные роли
community.set_default_roles(["reader"]) # Только reader по умолчанию
```
### Доступные роли
Сообщество может ограничить список доступных ролей:
```python
# Все роли доступны по умолчанию
available_roles = ["reader", "author", "artist", "expert", "editor", "admin"]
# Ограничить только базовыми ролями
community.set_available_roles(["reader", "author", "editor"])
```
## Миграция данных
### Проблемы существующих пользователей
1. **Пользователи без роли `reader`** - не могут войти в систему
2. **Старая система ролей** - данные в `Author.roles` устарели
3. **Отсутствие связей `CommunityAuthor`** - новые пользователи без ролей
### Решения
#### 1. Автоматическое добавление роли `reader`
```python
async def ensure_user_has_reader_role(user_id: int) -> bool:
"""Убеждается, что у пользователя есть роль 'reader'"""
existing_roles = get_user_roles_in_community(user_id, community_id=1)
if "reader" not in existing_roles:
success = assign_role_to_user(user_id, "reader", community_id=1)
if success:
logger.info(f"Роль 'reader' добавлена пользователю {user_id}")
return True
return True
```
#### 2. Массовое исправление ролей
```python
async def fix_all_users_reader_role() -> dict[str, int]:
"""Проверяет всех пользователей и добавляет роль 'reader'"""
stats = {"checked": 0, "fixed": 0, "errors": 0}
all_authors = session.query(Author).all()
for author in all_authors:
stats["checked"] += 1
try: try:
result = await some_rbac_protected_function() await ensure_user_has_reader_role(author.id)
except RBACError as e: stats["fixed"] += 1
return {"success": False, "error": str(e)} except Exception as e:
logger.error(f"Ошибка для пользователя {author.id}: {e}")
stats["errors"] += 1
return stats
``` ```
## Настройка сообщества #### 3. Миграция из старой системы
### Управление ролями в сообществе
```python ```python
from orm.community import Community def migrate_old_roles_to_community_author():
"""Переносит роли из старой системы в CommunityAuthor"""
community = session.query(Community).filter(Community.id == 1).first() # Получаем все старые роли из Author.roles
old_roles = session.query(AuthorRole).all()
# Установка доступных ролей for role in old_roles:
community.set_available_roles(['reader', 'author', 'expert', 'admin']) # Создаем запись CommunityAuthor
ca = CommunityAuthor(
community_id=role.community,
author_id=role.author,
roles=role.role
)
session.add(ca)
# Установка дефолтных ролей для новых участников session.commit()
community.set_default_roles(['reader'])
# Получение настроек
available = community.get_available_roles() # ['reader', 'author', 'expert', 'admin']
default = community.get_default_roles() # ['reader']
``` ```
### Автоматическое назначение дефолтных ролей ## API для работы с ролями
При создании связи пользователя с сообществом автоматически назначаются роли из `default_roles`.
## Интеграция с GraphQL контекстом ### 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
### Логирование
### Middleware для установки ролей
```python ```python
async def rbac_middleware(request, call_next): # Логирование изменений ролей
# Получаем автора из контекста logger.info(f"Role {role} assigned to user {user_id} by admin {admin_id}")
author = getattr(request.state, 'author', None) logger.warning(f"Failed login attempt for user without reader role: {user_id}")
if author: logger.error(f"Permission denied: user {user_id} tried to access {resource}")
# Устанавливаем роли в контекст для текущего сообщества
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'ах ## Тестирование
### Тестовые сценарии
1. **Регистрация пользователя** - проверка назначения дефолтных ролей
2. **Вход в систему** - проверка требования роли `reader`
3. **Назначение ролей** - проверка прав администратора
4. **Проверка разрешений** - валидация доступа к ресурсам
5. **Иерархия ролей** - наследование прав
### Пример тестов
```python ```python
def get_user_roles_from_context(info): def test_user_registration_assigns_default_roles():
"""Получение ролей пользователя из GraphQL контекста""" """Проверяет назначение дефолтных ролей при регистрации"""
# Из middleware user = create_user(email="test@test.com")
user_roles = getattr(info.context, "user_roles", []) roles = get_user_roles_in_community(user.id, community_id=1)
if user_roles:
return user_roles
# Из author'а напрямую assert "reader" in roles
author = getattr(info.context, "author", None) assert "author" in roles
if author and hasattr(author, "roles"):
return author.roles.split(",") if author.roles else []
return [] 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")
``` ```
## Миграция и обновления
### Миграция с предыдущей системы ролей
Если в проекте была отдельная таблица ролей, необходимо:
1. Создать миграцию для добавления поля `roles` в `CommunityAuthor`
2. Перенести данные из старых таблиц в CSV формат
3. Удалить старые таблицы ролей
```bash
alembic revision --autogenerate -m "Add CSV roles to CommunityAuthor"
alembic upgrade head
```
### Обновление CHANGELOG.md
После внесения изменений в RBAC систему обновляется `CHANGELOG.md` с новой версией.
## Производительность ## Производительность
### Оптимизация ### Оптимизации
- 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')
```

View File

@@ -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,10 +79,9 @@ 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):
@@ -92,12 +90,28 @@ class AuthService:
user_id_int = int(user_id) user_id_int = int(user_id)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число") logger.error(f"Невозможно преобразовать user_id {user_id} в число")
else: return 0, [], False
# Проверяем наличие админских прав через новую RBAC систему
# Получаем роли через новую систему CommunityAuthor
from orm.community import get_user_roles_in_community from orm.community import get_user_roles_in_community
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1) 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) 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"
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при проверке прав администратора: {e}") logger.error(f"Ошибка при проверке прав администратора: {e}")
@@ -112,27 +126,31 @@ 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
logger.debug("Using local authentication with new RBAC system")
with local_session() as session:
try: try:
author = session.query(Author).filter(Author.id == user_id).one() user_id_int = int(user_id)
except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
return None
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1) from orm.community import assign_role_to_user, get_user_roles_in_community
# Проверяем существующие роли
existing_roles = get_user_roles_in_community(user_id_int, community_id=1)
logger.debug(f"Существующие роли пользователя {user_id}: {existing_roles}")
# Добавляем новые роли через новую систему RBAC
for role_name in roles: for role_name in roles:
success = assign_role_to_user(int(user_id), role_name, community_id=1) if role_name not in existing_roles:
success = assign_role_to_user(user_id_int, role_name, community_id=1)
if success: if success:
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}") logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
else: else:
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}") logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
else:
logger.debug(f"Роль {role_name} уже есть у пользователя {user_id}")
return user_id return user_id
except exc.NoResultFound:
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:
"""Создает нового пользователя с дефолтными ролями""" """Создает нового пользователя с дефолтными ролями"""
user = Author(**user_dict) user = Author(**user_dict)
@@ -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'."""