web/favicon.svg повторяет .brand-logo шапки: синий скруглённый квадрат с белым глобусом (геометрия I.globe без изменений, пропорции логотипа), подключён в index.html. Итог 039 дополнен результатом pytest (17 passed) после включения учётной записи admin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IPAM Manager
Реестр IP-адресов и адресных префиксов в разрезе организаций. Проект построен по принципу API-first: backend на FastAPI и PostgreSQL,
UI-админка — лёгкий SPA без сборки (web/), который только отображает ответы API. Макеты — страница «IPAM Manager» дизайн-канваса ros_control.
Быстрый старт
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–0016), типы 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 друг друга не блокируют.
Устройства
- Эксплуатационный статус устройства (изменение 039): «Активен» (по умолчанию), «Выключен», «На обслуживании». Назначается вручную на экране устройств.
Адреса
- IP уникален в пределах VRF и хранится в самом узком содержащем его префиксе.
- Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя.
- Свободные адреса не хранятся, а вычисляются. В общем списке они показываются только для подсетей до /20.
- В списке адресов префикса видны тип, имя и статус привязанного устройства (изменение 039); у адресов без устройства — «—».
Ёмкость и «Обзор»
- Ёмкость префикса — размер его подсети (для 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.
- Caddy проксирует на опубликованный порт приложения (
- Переход на ролевую модель (миграция 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браузера. - «Устройства»: колонка «Статус» (бейдж) и поле «Статус» в диалоге добавления/редактирования; «Адреса» подсети: колонки «Тип устройства», «Устройство», «Статус устройства» после «Описание» (изменение 039).
- Переключатель организации — только у
superadmin;admin/viewerработают в своей организации. - Строка реестра кликабельна целиком. Действия над строкой — в меню «⋯».
- Групповые операции через чекбоксы (кроме «Журнала»): удаление, смена типа устройств, статус префиксов и адресов, доступ пользователей.
- Экран «Префиксы» загружает до 20 000 префиксов организации и сообщает, если загружены не все.
Тесты
Тесты работают с приложением и БД, заданными в .env, то есть с запущенным стендом: они создают и удаляют временные данные.
docker compose -p ipam_control_006 up -d --build && venv/bin/python -m pytest -q
История изменений
Каждая доработка описана в docs/changes/<номер>/: PLAN.md — план, SUMMARY.md — итог.
| № | Изменение | Документы |
|---|---|---|
| 001 | Backend и UI-админка | план · итог |
| 002 | Смена VRF у префикса, целостность «VRF ⊂ организация» | план · итог |
| 003 | Журнал: поиск, ротация, очистка | план · итог |
| 004 | Журнал: IP клиента и метаданные запроса | план · итог |
| 005 | Кликабельные строки реестров | план · итог |
| 006 | Раздел «Пользователи» | план · итог |
| 007 | Исправление удаления организации | план · итог |
| 008 | Журнал отклонённых удалений | план · итог |
| 009 | Групповые операции в UI | план · итог |
| 010 | Автовыделение вложенного префикса | план · итог |
| 011 | Уникальность IP в VRF | план · итог |
| 012 | Ограничение попыток входа | план · итог |
| 013 | Границы пагинации | план · итог |
| 014 | Производительность экрана адресов | план · итог |
| 015 | Запрет адреса сети и broadcast | план · итог |
| 016 | Роль по умолчанию — «Просмотр» | план · итог |
| 017 | Проверка секретов при старте | план · итог |
| 018 | null в PATCH |
план · итог |
| 019 | Устранение N+1 запросов | план · итог |
| 020 | Автоназначение адреса с учётом вложенных префиксов | план · итог |
| 021 | Блокировки: администраторы, регистр логина | план · итог |
| 022 | Эксплуатация: контейнер, миграции, TLS, зависимости | план · итог |
| 023 | Безопасность и журнал: мелкие улучшения | план · итог |
| 024 | Исправление находок ревью 011–023 | план · итог |
| 025 | Ёмкость частично разбитого префикса | план · итог |
| 026 | Политика блокировки входа | план · итог |
| 027 | Сериализация попыток входа по IP | план · итог |
| 028 | Экран «Префиксы» без усечения | план · итог |
| 029 | Целостность дерева префиксов | план · итог |
| 030 | Исправление замечаний ревью 025–029 | план · итог |
| 032 | Ролевая модель с привязкой к организации и суперадминистратором | план · итог |
| 033 | Исправление находок ревью 032 | план · итог |
| 034 | Публикация через Caddy: rxipam.rxmsk.ru | план · итог |
| 035 | Последний вход пользователя: дата и IP | план · итог |
| 036 | Меню пользователя в шапке: логин с выпадающим списком | план · итог |
| 037 | Двухфакторная аутентификация TOTP, по выбору пользователя | план · итог |
| 038 | Тёмная тема UI | план · итог |
| 039 | Статус устройства и сведения об устройстве в списке адресов префикса | план · итог |
| 040 | Название в UI: «IPAM Manager» | план · итог |
| 041 | Favicon из логотипа бренда | план · итог |
Отчёты ревью
- Ревью кодовой базы (находки → изменения 011–023)
- Ревью изменений 011–023 (→ 024)
- Повторный анализ кодовой базы (→ 025–029)
- Ревью и тестирование изменений 025–029 (→ 030)
- Ревью и тестирование изменения 030
- Пентест (чёрный ящик) (→ план 031, не реализован)
- Ревью и тестирование изменения 032 (→ 033)
- Ревью кодовой базы после 032 (→ 033)