Files
ipam_control/docs/changes/032-role-model-org-scope/PLAN.md
T
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

105 lines
15 KiB
Markdown
Raw 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.
# Ролевая модель с привязкой к организации и суперадминистратором (изменение 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`.