628a1d4536
- Модель данных: mchs_units (РЦУ РЧС → ОУМЧС → Г(Р)ОЧС, kind+parent_id), users +8 колонок (unit_id, position, status, lockout, must_change_password, created_by, updated_at), roles/permissions/role_permissions/user_roles (M2M матрица), sessions (server-side, token_hash=sha256(secret)), auth_events, security_settings. - Alembic 007_e1_rbac: схема + сид 4 ролей / 9 прав / матрицы (9/6/3/1) / 10 настроек безопасности; downgrade → upgrade идемпотентен (проверено на pg16-контейнере, сид не дублируется). - auth.py: login с lockout (5 неудач → 15 мин) и auth-журналом; server-side сессии (JWT несёт secret, в БД — sha256-хэш; max_concurrent_sessions); /me с permissions (user_roles → role_permissions, fallback legacy-роль); /change-password (мин. длина из settings, отзыв сессий); /logout. require_permission(code) — атомарные права (fallback legacy-роли). - Обратная совместимость: require_roles не тронут, старые JWT без sid продолжают работать. - Тесты E1 (9): login/logout/lockout/me-права/смена пароля/журнал. SQLite in-memory (StaticPool + компиляторы UUID/JSONB/ARRAY). 202 passed, 5 skipped.
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""E1 (B21): RBAC-ядро — подразделения МЧС, роли/права, сессии, auth-журнал.
|
||
|
||
Иерархия подразделений: РЦУ РЧС (rcu) → ОУМЧС (oblast) → Г(Р)ОЧС (gor_rayon).
|
||
Роли — как в РВС-админке (is_system), права атомарные, user_roles M2M.
|
||
Сессии server-side: JWT несёт session_id, каждый запрос проверяет сессию
|
||
в БД (отзыв по блокировке пользователя/окончании срока).
|
||
|
||
Модели добавлены в backend/models.py; здесь — только константы (seed-данные)
|
||
и хелперы прав/скоупа.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from sqlalchemy import text
|
||
|
||
# ---- Иерархия подразделений ----
|
||
UNIT_KINDS = ('rcu', 'oblast', 'gor_rayon')
|
||
|
||
# ---- Роли (is_system) ----
|
||
SYSTEM_ROLES = (
|
||
'admin', # РЦУ РЧС: все права, все операции
|
||
'coordinator', # ОУМЧС: поиски своей области, управление подчинёнными Г(Р)ОЧС
|
||
'operator', # Г(Р)ОЧС: свои поиски (create/update)
|
||
'observer', # только просмотр
|
||
)
|
||
|
||
# ---- Атомарные права ----
|
||
PERMISSIONS = (
|
||
'view', 'create', 'update', 'delete', 'export',
|
||
'manage_users', 'manage_roles', 'view_audit', 'manage_security',
|
||
)
|
||
|
||
ROLE_PERMISSIONS: dict[str, tuple[str, ...]] = {
|
||
'admin': PERMISSIONS,
|
||
'coordinator': ('view', 'create', 'update', 'export', 'manage_users', 'view_audit'),
|
||
'operator': ('view', 'create', 'update'),
|
||
'observer': ('view',),
|
||
}
|
||
|
||
# Роли-совместимость со старым единым role-полем: admin/coordinator/operator/observer
|
||
# покрывают прежние operator/field/admin (field ≈ operator: полевой доступ).
|
||
|
||
# ---- Политика безопасности (security_settings) ----
|
||
SECURITY_DEFAULTS = {
|
||
'password_min_length': '8',
|
||
'password_require_complexity': 'true',
|
||
'password_expiry_days': '90',
|
||
'password_history_count': '5',
|
||
'max_failed_logins': '5',
|
||
'lockout_duration_minutes': '15',
|
||
'session_timeout_minutes': '1440',
|
||
'session_idle_timeout_minutes': '60',
|
||
'max_concurrent_sessions': '3',
|
||
'audit_retention_days': '90',
|
||
}
|
||
|
||
|
||
def descendant_unit_ids(conn, unit_id: str) -> list[str]:
|
||
"""Все id подразделения + его потомков (для гео-скоупа). Рекурсивный CTE."""
|
||
rows = conn.execute(
|
||
text("""
|
||
WITH RECURSIVE tree AS (
|
||
SELECT id FROM mchs_units WHERE id = :uid
|
||
UNION ALL
|
||
SELECT u.id FROM mchs_units u JOIN tree t ON u.parent_id = t.id
|
||
)
|
||
SELECT id FROM tree
|
||
"""),
|
||
{'unit_id': unit_id},
|
||
).fetchall()
|
||
return [str(r[0]) for r in rows]
|