Files
ipam_control/README.md
T
ayurishchevandClaude Opus 5.5 13e17fbb47 Задачи 011-024: доработки по ревью кодовой базы и исправление находок
Ревью кодовой базы (docs/reviews/2026-09-26-codebase-review.md) и планы по каждой находке:
011 IP уникален в VRF и хранится в самом узком префиксе (addresses.vrf_id, составной FK
    с каскадом при переносе VRF, миграция 0007 с остановкой на дублях).
012 Ограничение попыток входа (login_attempts, 429 + Retry-After), выравнивание времени
    ответа, журнал без вытеснения анонимными событиями (миграция 0006).
013 Границы пагинации: отрицательные/чрезмерные limit/offset дают 422 вместо 500.
014 Экран адресов: страница свободных адресов арифметикой, пагинация в SQL.
015 Запрет адреса сети/broadcast, загрузка не выше 100 %.
016 Роль по умолчанию — viewer.
017 Проверка JWT_SECRET/ADMIN_PASSWORD при старте.
018 null в PATCH очищает текстовые поля; нейтральный текст конфликта БД.
019 Пакетная загрузка в списках вместо N+1.
020 Автоназначение адреса вне вложенных префиксов, с блокировкой префикса.
021 Advisory-lock при снятии прав администратора, уникальный lower(username) (миграция 0008).
022 Контейнер не от root, healthcheck, блокировка миграций, requirements.lock.
023 Экранирование LIKE, журнал отказов очистки, заголовки безопасности, учёт force-удаления,
    отзыв токенов при смене пароля (claim pv, миграция 0005).
024 Исправление находок ревью 011-023 (docs/reviews/2026-09-26-changes-011-023-review.md):
    сериализация попыток входа, запрет переноса адресов в адрес сети/broadcast, журнал входов,
    запрет смены своего пароля через PATCH, валидация PATCH устройства, обновлён тест токенов.

Тесты: 14 passed. Документация: README.md, docs/changes/011-024, docs/reviews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:33:50 +03:00

19 KiB
Raw Blame History

IPAM Manager

Реестр IP-адресов и адресных префиксов в разрезе организаций. API-first: backend на Python (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://127.0.0.1:8088/ · Swagger: http://127.0.0.1:8088/docs (порт приложения — APP_PORT в .env, слушает 0.0.0.0; порт БД — только 127.0.0.1)
  • Логин/пароль администратора — ADMIN_USERNAME / ADMIN_PASSWORD из .env (создаётся при первом старте; пароль не короче 8 символов).
  • JWT_SECRET обязателен и не короче 32 символов (заглушки отклоняются): без него приложение не стартует (изменение 017). scripts/gen_env.py генерирует корректные значения.
  • Порт приложения публикуется на ${APP_BIND:-0.0.0.0} (в .env можно задать APP_BIND=127.0.0.1 — только loopback, за reverse-proxy с TLS, см. «Публикация»).

Архитектура

Слой Технологии
API FastAPI, pydantic v2, JWT (argon2), роли admin (запись) / viewer (чтение)
БД PostgreSQL 16, SQLAlchemy 2, Alembic (alembic/versions), типы CIDR/INET
UI статический SPA (ES-модуль, vanilla JS), раздаётся приложением; шрифты IBM Plex — локально в web/fonts/ (woff2 latin+cyrillic, лицензия OFL), внешних зависимостей нет
app/  main.py config.py db.py security.py models.py schemas.py services.py api/v1/{auth,refs,prefixes,overview}.py
web/  index.html styles.css app.js
alembic/  scripts/{gen_env,seed_demo,find_duplicate_addresses,find_unusable_addresses}.py  requirements.lock  tests/  docs/changes/  docs/reviews/

Модель данных

organizations → vrfs (по организации, «default» создаётся автоматически) → prefixes (дерево через parent_id, вложенность определяется автоматически) → addresses; devices + device_types; isps + isp_networks; users; audit_log.

  • Ёмкость листового префикса — размер подсети (IPv4 без сетевого/broadcast); родителя — сумма вложенных листьев.
  • «Свободные» адреса не хранятся, а вычисляются; в списке адресов они показываются для подсетей до /20. Страница status=free считается арифметически (без перебора адресов, offset ≤ 10 000 000), остальные фильтры и постраничная выдача — в SQL (изменение 014).
  • IP уникален в пределах VRF и хранится в самом узком префиксе (изменение 011): addresses.vrf_id + составной FK (prefix_id, vrf_id) (обновляется каскадом при переносе VRF), уникальность (vrf_id, address). Адрес из диапазона вложенного префикса в родителе назначить нельзя (422), дубль в VRF → 409; новый вложенный префикс забирает адреса родителя из своего диапазона (moved_addresses в журнале); перенос префикса в VRF с тем же адресом → 409. Миграция 0007 останавливается при дублях — найти: scripts/find_duplicate_addresses.py.
  • Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя (422, изменение 015); уже внесённые найдёт scripts/find_unusable_addresses.py; загрузка не превышает 100 %. Перенос адресов при создании вложенного префикса или смене VRF, из-за которого адрес стал бы сетевым/broadcast в новом месте, тоже отклоняется 422 с перечислением адресов и целевых префиксов (изменение 024): создание префикса откатывается целиком, смена VRF — без частичных изменений.
  • Обзор считает использование только по IPv4.
  • VRF — часть адресного плана организации: имя уникально в пределах организации (без учёта регистра), в разных организациях имена могут совпадать; в одном VRF может быть много префиксов. Принадлежность VRF организации префикса гарантирует составной FK в БД.
  • Смена VRF у префикса (PATCH /prefixes/{id} с vrf_id) — только среди VRF той же организации; переносится префикс вместе с вложенными, дубль CIDR в целевом VRF → 409 (без частичных изменений), VRF другой организации → 422.
  • Автовыделение вложенного префикса (изменение 010): POST /prefixes/{id}/subnets/next с length (например 30) создаёт дочерний префикс в первом свободном выровненном блоке родителя (учитываются вложенные префиксы и адреса родителя); GET с тем же путём и ?length= — предпросмотр. Нет места → 409, недопустимый размер → 422. В UI — пункт «Добавить вложенный (авто)» в меню «⋯» префикса.
  • VRF, тип устройства, организация с зависимыми объектами не удаляются (409). У организации без префиксов, устройств и операторов служебный VRF default удаляется вместе с ней (исправлено в изменении 007: раньше такое удаление давало 409).

API (/api/v1)

POST /auth/login · GET /auth/me · GET /overview CRUD: /organizations, /vrfs, /isps, /device-types, /devices, /prefixes, /addresses/{id}, /users Адреса префикса: GET|POST /prefixes/{id}/addresses (status, q, limit, offset), POST …/addresses/next — автоназначение из пула (первый свободный адрес вне вложенных префиксов, без адреса сети/broadcast; префикс блокируется на время выдачи — изменение 020). Пагинация во всех списках: limit ≥ 1 (и не больше предела эндпоинта), offset от 0 до 10 000 000; иначе 422 (изменение 013). PATCH: null в текстовом поле очищает его (пустая строка), null в status адреса → 422, device_id: null отвязывает устройство (изменение 018). PATCH /devices/{id} проверяет name (hostname/FQDN) и mac (формат, нормализация к AA:BB:CC:DD:EE:FF) так же, как создание — раньше PATCH пропускал некорректные значения (изменение 024). Поиск (q) экранирует % и _. Ответы содержат заголовки Content-Security-Policy (кроме /docs), X-Content-Type-Options, X-Frame-Options, Referrer-Policy (изменение 023). Ошибки: {code, message, fields} (для блокировок/лимитов — дополнительные поля attempts_left, retry_after_seconds). Каждое изменение пишется в audit_log.

Пользователи и роли

  • GET|POST /users, PATCH|DELETE /users/{id} — управление учётными записями, только admin: логин (3–100 символов, латиница, цифры, . _ -), роль, доступ и пароль (при сбросе администратором). Логин после создания не меняется — он же sub в токене.
  • POST /users/me/password {current_password, new_password} — смена своего пароля, доступна любой роли; неверный текущий пароль → 403. Ответ содержит новый access_token: прежние токены пользователя после смены пароля (самим или администратором) недействительны (изменение 023, claim pv).
  • PATCH /users/{id} с password для своей же учётной записи → 422: свой пароль меняется только через /users/me/password (там обязательно подтверждение текущего пароля; изменение 024).
  • Занятый логин (в том числе в другом регистре) → 409; служебные логины system и anonymous, короткий логин или пароль → 422.
  • Свою учётную запись нельзя понизить, отключить или удалить, как и последнего активного администратора → 409.
  • Отключение действует немедленно (токен проверяется по users.is_active на каждом запросе). Роль viewer видит реестр и журнал, но любые изменения получает с 403.
  • Роль по умолчанию при создании — viewer (изменение 016). Логин уникален без учёта регистра и в БД (изменение 021); снятие прав администратора и удаление пользователей идут под advisory-lock.
  • Изменения пишутся в журнал: user.created, user.updated, user.password_reset, user.deleted (значения паролей не сохраняются).

Журнал

  • Отказ в удалении по бизнес-правилу (409: организация, VRF, тип устройства, префикс с адресами, пользователь) фиксируется событием <сущность>.delete_blocked (изменение 008); в «Данных» записи — reason и blocked_by со списками мешающих объектов (total и до 20 названий: префиксы, устройства, операторы, адреса).
  • GET /audit — поиск и фильтры: q (сообщение, метка объекта, начало ID записи), event_type (prefix.created), entity_type, actor (ui:admin, system, anonymous), date_from/date_to (UTC), limit/offset; GET /audit/summary, /audit/facets, /audit/{uid}.
  • GET|PUT /journal/settings — ротация: retention_days (по умолчанию 90) и max_entries (100 000), 0 — без ограничения. Ротация идёт раз в час и сразу при сохранении настроек (advisory-lock защищает от параллельного запуска); каждая ротация с удалениями фиксируется записью journal.rotated. Запись — только admin.
  • POST /journal/clear {password} — очистка с подтверждением пароля текущего пользователя (admin); 5 неверных попыток за 10 минут → блокировка на 10 минут (429). В журнале остаётся запись journal.cleared.
  • IP и метаданные запроса: каждая запись, созданная в рамках HTTP-запроса, хранит client_ip и meta (user_agent, method, path, request_id; ответ содержит X-Request-ID); системные события (ротация) — без IP. Фильтр GET /audit?client_ip= принимает IP или подсеть (192.168.5.0/24), текстовый поиск q ищет и по началу IP. IP берётся из адреса сокета. X-Forwarded-For учитывается только от прокси из TRUSTED_PROXIES (CIDR через запятую в .env, по умолчанию пусто) — иначе IP можно подделать. Запросы с самой машины через 127.0.0.1 Docker показывает адресом шлюза сети (172.x.0.1); с LAN-адреса и удалённых хостов виден реальный источник.
  • В журнал пишутся также входы (session.login, session.failed — актор anonymous, только первая неудача в окне по этому логину; session.locked — блокировка, diff.distinct_logins — число разных логинов с этого IP в окне, если сработал лимит по IP) и служебные события (journal.*, актор system; неверный пароль очистки — journal.clear_failed / journal.clear_locked). Удаление префикса с force=true фиксирует addresses_deleted.
  • Вход: 5 неудач на логин и 20 на IP за 10 минут → блокировка на 10 минут (429, Retry-After, retry_after_seconds; изменение 012). Для несуществующего логина время ответа выравнивается. Пароль ≤ 128 символов. Попытки одного логина сериализованы (pg_advisory_xact_lock) — параллельные запросы не обходят лимит (изменение 024). При ротации по количеству первыми удаляются session.failed и session.locked. За reverse-proxy без TRUSTED_PROXIES все клиенты делят один IP — лимит по IP заденет всех.

Публикация и эксплуатация

  • Контейнер приложения работает от непривилегированного пользователя (uid 10001), у сервиса есть healthcheck (/healthz), сервисы перезапускаются (restart: unless-stopped).
  • Миграции при старте выполняются под advisory-lock — параллельные реплики не гоняют их одновременно. Зависимости зафиксированы в requirements.lock (обновление: venv/bin/pip-compile --strip-extras -o requirements.lock requirements.txt).
  • TLS: приложение отдаёт HTTP; для эксплуатации поставьте reverse-proxy (пример для Caddy) и задайте APP_BIND=127.0.0.1, TRUSTED_PROXIES=<адрес прокси/сеть Docker>:
    ipam.example.com {
        reverse_proxy 127.0.0.1:8088
    }
    

Поведение таблиц UI

Администратор может выбирать строки чекбоксами (в шапке — «выбрать все») на экранах «Организации», «Операторы», «Устройства», «Префиксы» (листовые), «Адреса» (кроме «Свободен») и «Пользователи» (кроме себя); в «Журнале» выбора нет. Панель над таблицей: «Удалить» везде, «Сменить тип» (устройства), «Статус» (префиксы, адреса), «Разрешить/Отключить доступ» (пользователи). Операции идут по одному запросу на объект (изменение 009): итог «выполнено N из M», отказы (зависимые объекты, свой аккаунт, последний администратор) показаны списком с причиной, попадают в журнал как *.delete_blocked, и остаются выбранными. Префиксы удаляются без force: префикс с адресами удаляется из одиночного меню строки. Строка реестра кликабельна целиком (как в журнале): «Префиксы» — лист открывает адреса подсети, родитель сворачивает/разворачивает ветку; «Организации» — префиксы организации; «Операторы», «Устройства» и «Адреса» — окно редактирования (свободный адрес — «Назначить адрес» с этим IP). Ссылки, шеврон, меню «⋯» работают как раньше и не запускают действие строки; Ctrl/Shift+клик и выделение текста тоже игнорируются. Экран «Пользователи» доступен только администратору: создание, редактирование роли и доступа, удаление на месте, а свой пароль меняется кнопкой «Сменить пароль» в шапке.

Тесты

Идут против приложения в контейнерах, учётные данные берутся из .env:

docker compose up -d --build && venv/bin/python -m pytest -q