Files
ipam_control/README.md
T

203 lines
22 KiB
Markdown
Raw Normal View History

# 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` |
## Архитектура
| Слой | Технологии |
|---|---|
| API | FastAPI, pydantic v2, JWT (срок 8 ч), пароли в argon2, роли `superadmin`, `admin`, `viewer` с привязкой к организации |
| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (миграции `0001`–`0014`), типы `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`, `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` |
| Журнал | `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`.
**Журнал**
- Хранит 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); «—», если пользователь ещё не входил.
- Переключатель организации — только у `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) |
## Отчёты ревью
- [Ревью кодовой базы](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)