Files
Sadmin 628a1d4536 E1 (B21): RBAC-ядро — подразделения МЧС, роли/права, сессии, auth-журнал
- Модель данных: 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.
2026-09-09 21:11:38 +03:00

71 lines
2.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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]