Files
ipam_control/README.md
T
ayurishchevandClaude Opus 5.5 aeb89dbded Задача 040: название в UI — «IPAM Manager»
В web/app.js три вхождения «ipam_manager» (шапка, экран входа и шаг 2FA) заменены
на «IPAM Manager». Атомарная правка отдельным коммитом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:01:58 +03:00

219 lines
25 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.
# IPAM Manager
Реестр IP-адресов и адресных префиксов в разрезе организаций. Проект построен по принципу API-first: backend на FastAPI и PostgreSQL,
UI-админка — лёгкий SPA без сборки (`web/`), который только отображает ответы API. Макеты — страница «IPAM Manager» дизайн-канваса ros_control.
## Быстрый старт
```bash
python3 scripts/gen_env.py # .env со случайными паролями и секретами (файл в .gitignore)
docker compose up -d --build # postgres + app; миграции применяются при старте
python3 -m venv venv && venv/bin/pip install -r requirements-dev.txt
venv/bin/python scripts/seed_demo.py # по желанию: демо-данные из макетов
```
- UI: `http://<хост>:8088/`, Swagger: `http://<хост>:8088/docs`.
- Вход: `ADMIN_USERNAME` / `ADMIN_PASSWORD` из `.env`. Суперадминистратор создаётся при первом старте на пустой БД.
## Конфигурация (`.env`)
| Переменная | Назначение |
|---|---|
| `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | База данных |
| `JWT_SECRET` | Ключ подписи токенов. Обязателен, не короче 32 символов, заглушки отклоняются: без корректного значения приложение не стартует |
| `ADMIN_USERNAME`, `ADMIN_PASSWORD` | Первый суперадминистратор; пароль не короче 8 символов |
| `APP_PORT` | Порт UI и API на хосте (8088) |
| `APP_BIND` | Адрес публикации порта: по умолчанию `0.0.0.0`; `127.0.0.1` — только за reverse-proxy |
| `DB_HOST_PORT` | Порт PostgreSQL на хосте, публикуется только на `127.0.0.1` |
| `TRUSTED_PROXIES` | CIDR доверенных прокси через запятую; только от них принимается `X-Forwarded-For` |
| `TOTP_ENC_KEY` | Ключ Fernet для шифрования секретов 2FA (изменение 037). Не задан — приложение стартует, но 2FA недоступна (503); некорректный ключ — отказ старта; потеря ключа означает сброс 2FA всем пользователям |
## Архитектура
| Слой | Технологии |
|---|---|
| API | FastAPI, pydantic v2, JWT (срок 8 ч), пароли в argon2, роли `superadmin`, `admin`, `viewer` с привязкой к организации |
| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (миграции `0001`–`0015`), типы `CIDR`/`INET` |
| UI | Статический SPA (vanilla JS, ES-модуль) раздаётся приложением; шрифты IBM Plex хранятся локально, внешних зависимостей нет |
```
app/ main.py config.py db.py security.py models.py schemas.py services.py request_context.py rotation.py
app/api/v1/ auth.py refs.py prefixes.py overview.py journal.py users.py
web/ index.html styles.css app.js fonts/
alembic/ миграции схемы
scripts/ gen_env.py seed_demo.py find_duplicate_addresses.py find_unusable_addresses.py
tests/ автотесты (pytest)
docs/changes/ планы и итоги доработок docs/reviews/ отчёты ревью
```
## Модель данных и правила
`organizations` → `vrfs` → `prefixes` (дерево) → `addresses`; `devices` + `device_types`; `isps` + `isp_networks`; `users`; `audit_log`.
**Пользователи и организации**
- `admin`/`viewer` привязаны к одной организации (`organization_id`), `superadmin` — ни к одной. БД допускает `admin`/`viewer` без организации только у отключённой записи (состояние после миграции 0011).
- Смена роли на `superadmin` снимает организацию автоматически; понижение до `admin`/`viewer` требует указать `organization_id`.
- Изоляция: `admin`/`viewer` видят и меняют только данные своей организации, включая «Обзор», журнал и счётчики типов устройств. Чужой объект неотличим от несуществующего — 404.
**VRF и префиксы**
- У каждой организации автоматически создаётся VRF `default`. Имя VRF уникально в пределах организации без учёта регистра.
- Префикс принадлежит VRF своей организации; это гарантирует составной FK в БД.
- Вложенность префиксов (`parent_id`) определяется автоматически по CIDR. Смена VRF переносит префикс вместе с вложенными, только в пределах организации.
- Автовыделение вложенного префикса: система выбирает первый свободный выровненный блок заданного размера.
- Изменения дерева одного VRF выполняются по одному (advisory-lock); разные VRF друг друга не блокируют.
**Адреса**
- IP уникален в пределах VRF и хранится в самом узком содержащем его префиксе.
- Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя.
- Свободные адреса не хранятся, а вычисляются. В общем списке они показываются только для подсетей до /20.
**Ёмкость и «Обзор»**
- Ёмкость префикса — размер его подсети (для IPv4 без адреса сети и broadcast). Занятость считается по всему поддереву.
- «Обзор» учитывает только IPv4: ёмкость — сумма корневых активных префиксов, назначенные адреса — адреса внутри них.
**Удаление**
- Объекты с зависимыми данными не удаляются (409). Отказ фиксируется в журнале с перечнем мешающих объектов.
- Организация не удаляется, если у неё есть привязанные пользователи.
**Журнал аудита**
- Событие привязано к организации своей сущности (`organization_id`); `admin`/`viewer` видят только события своей организации, включая отклонённые удаления.
- Системные события (пользователи, вход, настройки и очистка журнала, типы устройств) не привязаны к организации и видны только `superadmin`.
- События организации показывают IP и User-Agent исполнителя, в том числе суперадминистратора.
- После удаления организации её события сохраняются с `organization_id = NULL`.
## API (`/api/v1`)
| Область | Эндпоинты |
|---|---|
| Вход | `POST /auth/login`, `POST /auth/login/2fa` (изменение 037), `GET /auth/me` |
| Справочники | `/organizations`, `/vrfs`, `/isps`, `/device-types`, `/devices` |
| Префиксы | `/prefixes`, `GET\|POST /prefixes/{id}/subnets/next` (предпросмотр и автовыделение вложенного) |
| Адреса | `GET\|POST /prefixes/{id}/addresses`, `POST /prefixes/{id}/addresses/next` (автоназначение из пула), `PATCH\|DELETE /addresses/{id}` |
| Пользователи | `/users`, `POST /users/me/password`, `POST /users/me/2fa/setup\|enable\|disable`, `POST /users/{id}/2fa/reset` (изменение 037) |
| Журнал | `GET /audit`, `/audit/summary`, `/audit/facets`, `/audit/{uid}`, `GET\|PUT /journal/settings`, `POST /journal/clear` |
| Сводка | `GET /overview` |
**Соглашения**
- Ошибки возвращаются в формате `{code, message, fields}`; для лимитов добавляются `retry_after_seconds` и `attempts_left`.
- Пагинация: `limit` ≥ 1, `offset` от 0 до 10 000 000.
- `null` в текстовом поле `PATCH` очищает его.
- Каждое изменение данных пишется в журнал аудита.
## Безопасность
**Роли и учётные записи**
| Роль | Права |
|---|---|
| `superadmin` | Все организации; пользователи, создание и удаление организаций, типы устройств, настройки и очистка журнала |
| `admin` | Запись в своей организации: VRF, префиксы, адреса, устройства, операторы, карточка организации |
| `viewer` | Чтение своей организации. Роль по умолчанию |
- Логин уникален без учёта регистра.
- Свою учётную запись нельзя понизить, отключить или удалить; последний активный `superadmin` защищён от понижения, отключения и удаления.
**Пароли и токены**
- Смена пароля отзывает ранее выданные токены. Свой пароль меняется только с подтверждением текущего.
**Вход**
- Лимит неудачных попыток за 10 минут:
- 5 — на пару логин + IP;
- 20 — на IP;
- 50 — на логин со всех IP, кроме тех, с которых пользователь успешно входил за 30 дней.
- Сверх лимита — 429 с `Retry-After`.
- Двухфакторная аутентификация TOTP — по желанию пользователя (изменение 037), не обязательна. Включается в меню логина
в шапке: пароль → QR-код (Google Authenticator, Aegis, 1Password, Bitwarden и подобные) → код подтверждения →
10 одноразовых кодов восстановления (показываются один раз). При включённой 2FA вход идёт в два шага:
`POST /auth/login` возвращает `mfa_token` вместо токена доступа, `POST /auth/login/2fa` принимает код из приложения
или код восстановления. Неверный код учитывается в тех же лимитах перебора, что и пароль. Секрет хранится в БД
зашифрованным ключом `TOTP_ENC_KEY`; коды восстановления — хэшем sha256. Суперадминистратор может сбросить 2FA
другому пользователю (`POST /users/{id}/2fa/reset`), не отключая свою.
**Журнал**
- Хранит IP клиента и метаданные запроса.
- Ротация по сроку (90 дней) и по количеству записей (100 000); значения настраиваются.
- Очистка журнала требует пароль.
**Ответы**
- Заголовки CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`.
## Публикация и эксплуатация
- Контейнер приложения работает от непривилегированного пользователя, у него есть healthcheck (`/healthz`) и перезапуск `unless-stopped`.
- Миграции выполняются при старте под advisory-lock. Зависимости зафиксированы в `requirements.lock`
(обновление: `venv/bin/pip-compile --strip-extras -o requirements.lock requirements.txt`).
- Приложение отдаёт HTTP, TLS обеспечивает reverse-proxy. Стенд опубликован как `https://rxipam.rxmsk.ru` через общий Caddy хоста
(`/opt/lvraid/apps/caddy/Caddyfile`, блок `rxipam.rxmsk.ru`):
- Caddy проксирует на опубликованный порт приложения (`172.19.0.1:8088` — шлюз его Docker-сети). Сети не объединяются:
в сети Caddy уже есть сервисы с именами `app` и `db`.
- Из интернета доступны UI и `/api/v1`. `/docs`, `/redoc`, `/openapi.json` — только из частных сетей (RFC 1918/4193, loopback), иначе 404.
- `TRUSTED_PROXIES=172.16.0.0/12` в `.env`: Caddy приходит в приложение с адреса шлюза Docker-моста, реальный IP клиента берётся
из `X-Forwarded-For`. Без этого все интернет-клиенты делят один IP — и лимит входа, и журнал.
- LAN-клиенты по публичному имени идут через hairpin NAT шлюза и видны как `192.168.5.253`. Чтобы видеть их реальные адреса,
нужен split DNS: `rxipam.rxmsk.ru` → `192.168.5.9` внутри LAN.
- Переход на ролевую модель (миграция 0011): прежние `admin` становятся `superadmin`, прежние `viewer` отключаются до назначения организации суперадминистратором.
- Перед обновлением рабочей БД проверьте данные скриптами только для чтения: `scripts/find_duplicate_addresses.py` и `scripts/find_unusable_addresses.py`.
- Имя compose-проекта задаётся флагом `-p`. Текущий стенд поднят как `ipam_control_006` (`docker compose -p ipam_control_006 …`); без флага команды работают с проектом `ipam_control`.
## Интерфейс
- Экраны: «Обзор», «Префиксы» (дерево по VRF), «Адреса» подсети, «Организации», «Операторы», «Устройства», «Журнал», «Пользователи» (только `superadmin`).
- В «Пользователях» видна дата и IP последнего входа (изменение 035); «—», если пользователь ещё не входил.
- Логин в шапке раскрывает меню «Сменить пароль» / «Двухфакторная аутентификация» / «Выйти» (изменения 036, 037).
- В «Пользователях» у записей с включённой 2FA — бейдж «2FA»; в меню строки суперадминистратора для чужих записей — «Сбросить 2FA» (изменение 037).
- Тёмная тема (изменение 038): по умолчанию следует настройке ОС/браузера (`prefers-color-scheme`); переключатель-пиктограмма в шапке (солнце/луна/монитор), выбор хранится в `localStorage` браузера.
- Переключатель организации — только у `superadmin`; `admin`/`viewer` работают в своей организации.
- Строка реестра кликабельна целиком. Действия над строкой — в меню «⋯».
- Групповые операции через чекбоксы (кроме «Журнала»): удаление, смена типа устройств, статус префиксов и адресов, доступ пользователей.
- Экран «Префиксы» загружает до 20 000 префиксов организации и сообщает, если загружены не все.
## Тесты
Тесты работают с приложением и БД, заданными в `.env`, то есть с запущенным стендом: они создают и удаляют временные данные.
```bash
docker compose -p ipam_control_006 up -d --build && venv/bin/python -m pytest -q
```
## История изменений
Каждая доработка описана в `docs/changes/<номер>/`: `PLAN.md` — план, `SUMMARY.md` — итог.
| № | Изменение | Документы |
|---|---|---|
| 001 | Backend и UI-админка | [план](docs/changes/001-ipam-backend/PLAN.md) · [итог](docs/changes/001-ipam-backend/SUMMARY.md) |
| 002 | Смена VRF у префикса, целостность «VRF ⊂ организация» | [план](docs/changes/002-prefix-vrf-change/PLAN.md) · [итог](docs/changes/002-prefix-vrf-change/SUMMARY.md) |
| 003 | Журнал: поиск, ротация, очистка | [план](docs/changes/003-journal-search-rotation/PLAN.md) · [итог](docs/changes/003-journal-search-rotation/SUMMARY.md) |
| 004 | Журнал: IP клиента и метаданные запроса | [план](docs/changes/004-audit-client-ip/PLAN.md) · [итог](docs/changes/004-audit-client-ip/SUMMARY.md) |
| 005 | Кликабельные строки реестров | [план](docs/changes/005-clickable-rows/PLAN.md) · [итог](docs/changes/005-clickable-rows/SUMMARY.md) |
| 006 | Раздел «Пользователи» | [план](docs/changes/006-users-management/PLAN.md) · [итог](docs/changes/006-users-management/SUMMARY.md) |
| 007 | Исправление удаления организации | [план](docs/changes/007-org-delete-fix/PLAN.md) · [итог](docs/changes/007-org-delete-fix/SUMMARY.md) |
| 008 | Журнал отклонённых удалений | [план](docs/changes/008-blocked-delete-audit/PLAN.md) · [итог](docs/changes/008-blocked-delete-audit/SUMMARY.md) |
| 009 | Групповые операции в UI | [план](docs/changes/009-bulk-actions/PLAN.md) · [итог](docs/changes/009-bulk-actions/SUMMARY.md) |
| 010 | Автовыделение вложенного префикса | [план](docs/changes/010-next-free-prefix/PLAN.md) · [итог](docs/changes/010-next-free-prefix/SUMMARY.md) |
| 011 | Уникальность IP в VRF | [план](docs/changes/011-address-unique-in-vrf/PLAN.md) · [итог](docs/changes/011-address-unique-in-vrf/SUMMARY.md) |
| 012 | Ограничение попыток входа | [план](docs/changes/012-login-rate-limit/PLAN.md) · [итог](docs/changes/012-login-rate-limit/SUMMARY.md) |
| 013 | Границы пагинации | [план](docs/changes/013-pagination-bounds/PLAN.md) · [итог](docs/changes/013-pagination-bounds/SUMMARY.md) |
| 014 | Производительность экрана адресов | [план](docs/changes/014-addresses-listing-performance/PLAN.md) · [итог](docs/changes/014-addresses-listing-performance/SUMMARY.md) |
| 015 | Запрет адреса сети и broadcast | [план](docs/changes/015-network-broadcast-addresses/PLAN.md) · [итог](docs/changes/015-network-broadcast-addresses/SUMMARY.md) |
| 016 | Роль по умолчанию — «Просмотр» | [план](docs/changes/016-default-role-viewer/PLAN.md) · [итог](docs/changes/016-default-role-viewer/SUMMARY.md) |
| 017 | Проверка секретов при старте | [план](docs/changes/017-jwt-secret-validation/PLAN.md) · [итог](docs/changes/017-jwt-secret-validation/SUMMARY.md) |
| 018 | `null` в PATCH | [план](docs/changes/018-patch-null-handling/PLAN.md) · [итог](docs/changes/018-patch-null-handling/SUMMARY.md) |
| 019 | Устранение N+1 запросов | [план](docs/changes/019-n-plus-one-queries/PLAN.md) · [итог](docs/changes/019-n-plus-one-queries/SUMMARY.md) |
| 020 | Автоназначение адреса с учётом вложенных префиксов | [план](docs/changes/020-next-free-address/PLAN.md) · [итог](docs/changes/020-next-free-address/SUMMARY.md) |
| 021 | Блокировки: администраторы, регистр логина | [план](docs/changes/021-concurrency-locks/PLAN.md) · [итог](docs/changes/021-concurrency-locks/SUMMARY.md) |
| 022 | Эксплуатация: контейнер, миграции, TLS, зависимости | [план](docs/changes/022-ops-hardening/PLAN.md) · [итог](docs/changes/022-ops-hardening/SUMMARY.md) |
| 023 | Безопасность и журнал: мелкие улучшения | [план](docs/changes/023-minor-hardening/PLAN.md) · [итог](docs/changes/023-minor-hardening/SUMMARY.md) |
| 024 | Исправление находок ревью 011–023 | [план](docs/changes/024-review-fixes-011-023/PLAN.md) · [итог](docs/changes/024-review-fixes-011-023/SUMMARY.md) |
| 025 | Ёмкость частично разбитого префикса | [план](docs/changes/025-prefix-capacity/PLAN.md) · [итог](docs/changes/025-prefix-capacity/SUMMARY.md) |
| 026 | Политика блокировки входа | [план](docs/changes/026-login-lockout-policy/PLAN.md) · [итог](docs/changes/026-login-lockout-policy/SUMMARY.md) |
| 027 | Сериализация попыток входа по IP | [план](docs/changes/027-login-ip-serialization/PLAN.md) · [итог](docs/changes/027-login-ip-serialization/SUMMARY.md) |
| 028 | Экран «Префиксы» без усечения | [план](docs/changes/028-prefixes-ui-pagination/PLAN.md) · [итог](docs/changes/028-prefixes-ui-pagination/SUMMARY.md) |
| 029 | Целостность дерева префиксов | [план](docs/changes/029-prefix-tree-lock/PLAN.md) · [итог](docs/changes/029-prefix-tree-lock/SUMMARY.md) |
| 030 | Исправление замечаний ревью 025–029 | [план](docs/changes/030-review-fixes-025-029/PLAN.md) · [итог](docs/changes/030-review-fixes-025-029/SUMMARY.md) |
| 032 | Ролевая модель с привязкой к организации и суперадминистратором | [план](docs/changes/032-role-model-org-scope/PLAN.md) · [итог](docs/changes/032-role-model-org-scope/SUMMARY.md) |
| 033 | Исправление находок ревью 032 | [план](docs/changes/033-review-fixes-032/PLAN.md) · [итог](docs/changes/033-review-fixes-032/SUMMARY.md) |
| 034 | Публикация через Caddy: rxipam.rxmsk.ru | [план](docs/changes/034-caddy-publication/PLAN.md) · [итог](docs/changes/034-caddy-publication/SUMMARY.md) |
| 035 | Последний вход пользователя: дата и IP | [план](docs/changes/035-user-last-login/PLAN.md) · [итог](docs/changes/035-user-last-login/SUMMARY.md) |
| 036 | Меню пользователя в шапке: логин с выпадающим списком | [план](docs/changes/036-user-menu/PLAN.md) · [итог](docs/changes/036-user-menu/SUMMARY.md) |
| 037 | Двухфакторная аутентификация TOTP, по выбору пользователя | [план](docs/changes/037-totp-2fa/PLAN.md) · [итог](docs/changes/037-totp-2fa/SUMMARY.md) |
| 038 | Тёмная тема UI | [план](docs/changes/038-dark-theme/PLAN.md) · [итог](docs/changes/038-dark-theme/SUMMARY.md) |
| 040 | Название в UI: «IPAM Manager» | [план](docs/changes/040-brand-name/PLAN.md) · [итог](docs/changes/040-brand-name/SUMMARY.md) |
## Отчёты ревью
- [Ревью кодовой базы](docs/reviews/2026-09-26-codebase-review.md) (находки → изменения 011–023)
- [Ревью изменений 011–023](docs/reviews/2026-09-26-changes-011-023-review.md) (→ 024)
- [Повторный анализ кодовой базы](docs/reviews/2026-09-26-codebase-review-2.md) (→ 025–029)
- [Ревью и тестирование изменений 025–029](docs/reviews/2026-09-26-changes-025-029-review.md) (→ 030)
- [Ревью и тестирование изменения 030](docs/reviews/2026-09-27-changes-030-review.md)
- [Пентест (чёрный ящик)](docs/reviews/2026-09-27-pentest.md) (→ план 031, не реализован)
- [Ревью и тестирование изменения 032](docs/reviews/2026-09-27-changes-032-review.md) (→ 033)
- [Ревью кодовой базы после 032](docs/reviews/2026-09-27-codebase-review.md) (→ 033)