# 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)