Files
ayurishchevandClaude Opus 5.5 744a025960 Задачи 032-033: ролевая модель с привязкой к организации, исправления по ревью
Пентест (docs/reviews/2026-09-27-pentest.md) и план 031 (Swagger, TLS) — план, не реализован.
032 Роль superadmin (без организации) и привязка admin/viewer к одной организации:
    users.organization_id + CHECK, audit_log.organization_id (миграции 0010-0012);
    require_org/scope_org во всех чтениях и записях, журнал и «Обзор» в границах
    организации; пользователи, организации, типы устройств, настройки журнала — только superadmin.
033 Исправление находок ревью 032 (docs/reviews/2026-09-27-changes-032-review.md,
    docs/reviews/2026-09-27-codebase-review.md):
    - FK audit_log.organization_id ON DELETE SET NULL (миграция 0013) — удаление организаций;
    - проверка организации в предпросмотре подсети;
    - инвариант «роль — организация» по итоговому состоянию (повышение снимает организацию,
      понижение требует её), 422/404 вместо обезличенных 409;
    - одинаковый 404 для чужих и несуществующих объектов (VRF, устройство, parent_id, оператор);
    - отказы удаления в журнале организации, счётчики типов в пределах организации;
    - UI: живое поле «Организация» в диалоге пользователя, бейдж superadmin; род в текстах 404.
README актуализирован под ролевую модель.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:26:57 +03:00

15 KiB
Raw Permalink Blame History

Ролевая модель с привязкой к организации и суперадминистратором (изменение 032)

Context

Сейчас роль пользователя (admin/viewer) действует на все организации сразу — любой администратор видит и меняет данные любой организации, включая журнал аудита. Нужна привязка учётной записи к одной организации: admin/viewer работают только в её пределах, включая чтение (реестры, «Обзор», журнал). Добавляется роль superadmin — без привязки к организации, видит и меняет всё, и единственный, кто создаёт/меняет пользователей (включая назначение админов организациям). Уровней прав внутри организации по-прежнему два: чтение или запись.

Решения (согласованы с пользователем)

  1. Управление пользователями — только superadmin. Администратор организации не создаёт и не редактирует других пользователей (даже viewer своей организации).
  2. Типы устройств (device_types) остаются общим справочником. Читают все роли из любой организации; создание/переименование/удаление — только superadmin.
  3. Журнал аудита изолирован по организации. Админ/viewer организации видит в журнале только события своей организации. Системные события без привязки к организации (вход/выход, настройки/очистка журнала, ротация, управление пользователями) видит только superadmin.
  4. Миграция существующих пользователей: все нынешние пользователи с ролью admin становятся superadmin (organization_id = NULL). Пользователи с ролью viewer остаются viewer, organization_id = NULL, is_active = false (учётная запись заблокирована), пока superadmin не назначит им организацию явным PATCH.

Модель данных

  • Role: добавить superadmin. Enum в PostgreSQL расширяется отдельной миграцией (ALTER TYPE ... ADD VALUE вне транзакции — autocommit_block()), использовать новое значение можно только в следующей миграции.
  • users.organization_id (nullable, FK organizations.id, индекс). Инвариант: superadmin ⇒ organization_id IS NULL; admin/viewer ⇒ organization_id задан. На уровне БД — CheckConstraint, ослабленный для неактивных записей (is_active = false OR (role = 'superadmin') = (organization_id IS NULL)), чтобы допустить временное состояние «viewer без организации, вход заблокирован» после миграции. На уровне API (UserIn/UserUpdate) инвариант строгий и без исключений — заблокированное состояние возникает только из миграции, не создаётся через API.
  • audit_log.organization_id (nullable, индекс). Заполняется у новых записей, если у сущности события есть организация (organization, vrf, prefix, address, device, isp); у user/journal/session/device_type — NULL (видно только superadmin). Для существующих записей — точечный backfill join'ом по entity_id там, где сущность ещё существует (organization, vrf, device, isp, prefix); что не сматчилось (сущность уже удалена) — остаётся NULL, то есть уходит в «видно только superadmin». Это осознанный компромисс, не полная реконструкция истории.
  • Удаление организации (delete_org) получает новую группу блокираторов — users: пока у организации есть привязанные пользователи (активные или нет), удалить её нельзя (иначе organization_id осиротеет или нарушится FK).

Миграции: 0010_user_role_superadmin.py (значение enum), 0011_users_organization_scope.py (колонка, ограничение, backfill ролей по решению 4), 0012_audit_log_organization.py (колонка, индекс, backfill).

Права доступа (сводка)

Действие Кто
Пользователи: список/создание/правка/удаление только superadmin
Организации: создание/удаление только superadmin
Организации: правка карточки superadmin или admin своей организации
VRF, префиксы, адреса, устройства, операторы: чтение/запись admin/viewer — только своя организация; superadmin — любая
Типы устройств: чтение любая роль, любая организация
Типы устройств: создание/правка/удаление только superadmin
Настройки/очистка журнала (/journal/settings, /journal/clear) только superadmin (затрагивает весь журнал)
Журнал: чтение (/audit*) admin/viewer — только события своей организации; superadmin — все, включая системные
«Обзор» admin/viewer — сводка по своей организации; superadmin — по всем

Реализация

app/models.py — Role.superadmin; User.organization_id + CheckConstraint; AuditLog.organization_id.

app/security.py — admin_user меняет смысл: пропускает admin и superadmin (роль ≠ viewer), название и место в коде не меняются. Новая зависимость superadmin_user (строго Role.superadmin) — по образцу admin_user.

app/services.py — переиспользуемые хелперы рядом с get_or_404/refuse_delete:

  • require_org(user, organization_id, what) — для операций с известным целевым id: superadmin пропускает, иначе при несовпадении HTTPException(404, f"{what} не найден") (404, а не 403 — не подтверждать существование чужих данных, тот же стиль сообщений, что у get_or_404).
  • scope_org(stmt, column, user) — для списков: у superadmin не трогает stmt, иначе stmt.where(column == user.organization_id) (безусловно, любой переданный клиентом organization_id в query-параметрах просто пересекается с этим условием — отдельной ошибки не нужно).
  • audit() получает необязательный organization_id (по умолчанию None) — простановка на стороне вызывающего кода, не выводится автоматически.
  • _GROUPS/blockers — добавить "users": "пользователи" для группы блокираторов удаления организации.

app/api/v1/users.py — все маршруты на superadmin_user. UserIn/UserUpdate (app/schemas.py) получают organization_id: int | None с валидацией «обязателен для admin/viewer, запрещён для superadmin» (model_validator), в UserOut — тоже поле. Инвариант «нельзя обезвредить последнего активного администратора» переводится на superadmin: защищается последний активный superadmin, не «последний admin организации» (в организации администраторов назначает и переназначает superadmin по своему усмотрению — не защищается отдельно, это осознанно, см. «Вне объёма»).

app/api/v1/refs.py — организации: create_org/delete_org → superadmin_user; update_org → admin_user + require_org; list_orgs/get_org → scope_org/require_org. VRF, устройства, операторы: та же пара admin_user + require_org/scope_org по образцу друг друга (один раз описать паттерн, применить в create_vrf/update_vrf/delete_vrf/list_vrfs, create_device/update_device/list_devices/get_device, аналогично isps). Типы устройств: чтение без изменений (уже открыто всем), запись → superadmin_user.

app/api/v1/prefixes.py — префиксы и адреса: require_org/scope_org там, где сейчас читается/проверяется organization_id (создание — из body, остальные операции — через уже загруженный префикс/родителя). list_prefixes/list_addresses(через принадлежащий префикс) — scope_org.

app/api/v1/journal.py — list_audit, /audit/summary, /audit/facets, GET /audit/{uid} — scope_org (для {uid} — require_org с 404 при чужой или системной записи). /journal/settings, /journal/clear → superadmin_user.

app/api/v1/overview.py — _ipv4_roots и подсчёт назначенных/зарезервированных адресов, recent_changes — фильтр по organization_id для не-superadmin (системные события без организации в recent_changes не подмешиваются).

app/main.py — seed(): пользователь-бутстрап из .env создаётся с role=Role.superadmin, organization_id=None.

web/app.js — ROLE_RU + "Суперадминистратор"; isAdmin() = роль ≠ viewer (как и на сервере), новая isSuperadmin(). Пункт навигации «Пользователи» — виден только isSuperadmin(). orgSwitcher() — интерактивный (с выпадающим списком) только для isSuperadmin(); для admin/viewer показывает название их единственной организации без переключателя (GET /organizations для них и так вернёт один элемент). Экран «Организации»: кнопка «Добавить организацию» и пункт «Удалить» в меню строки — только isSuperadmin(); редактирование доступно как обычная запись. Экран «Пользователи»: диалог создания/правки получает выбор организации (скрывается при роли «Суперадминистратор», обязателен для «Администратор»/«Просмотр»). Диалог типов устройств: кнопки добавления/переименования/удаления — только isSuperadmin(), список остаётся видимым всем.

scripts/seed_demo.py — создание демо-пользователей проставляет organization_id для ролей admin/viewer (иначе скрипт сломает новая валидация схемы).

README.md — разделы «Безопасность» (роли, права по организациям, superadmin) и «Модель данных» (новые поля/ограничения).

Вне объёма (сознательно)

  • Защита «последнего администратора организации» — не вводится; единственный защищаемый инвариант — последний активный superadmin.
  • Полная историческая реконструкция organization_id в старых записях журнала для уже удалённых сущностей — остаются NULL (только superadmin).
  • Типы устройств не разбиваются по организациям (решение 2).
  • Автотесты и правка существующих (tests/test_users.py и др., которые создают пользователей без organization_id) — на этапе тестирования отдельно, не сейчас.

Проверка (на этапе тестирования, не при реализации)

  • Импорт приложения, синтаксис UI, миграции 0010–0012 применяются, alembic check без расхождений.
  • Сценарии: superadmin создаёт организацию и в ней admin; этот admin видит/меняет только свою организацию (префиксы, устройства, журнал, «Обзор»), запросы к чужой организации получают 404; viewer организации только читает; superadmin управляет типами устройств, admin организации их только видит; удаление организации с привязанными пользователями отклоняется с перечнем; после миграции унаследованный admin из .env работает как superadmin.