Files
ipwhitelist-app/docs/ТЗ-реализация.md
naeel a8a07fe019 feat: IAM API интеграция + обновление ТЗ + тесты
- src/auth.js: fetchIamUser(), switchProfile(), userFromPayload(iamData)
- src/config.js: IAM_API_URL из env
- src/routes/oidc.js: обогащение сессии через IAM (с fallback на JWT)
- ui/routes/auth.js: обогащение при токен-логине
- ui/routes/entries.js: switchTo через POST /switch-profile
- src/api/routes/entries.js: resolveCompany через profiles[]
- views/index.ejs: переключатель компаний с названиями из profiles
- .env.example: IAM_API_URL
- docs: обновлены ТЗ-реализация.md, ТЗ-плюс.md, добавлен iam-integration.md
- tests: api-crud.sh (14 тестов CRUD + валидации)
- .gitignore: исключены .env.test, DEPLOY-TESTING.md
2026-06-04 13:53:44 +03:00

21 KiB
Raw Permalink Blame History

Техническое задание Микросервис управления доверенными адресами клиентов Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей, исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак

1. Назначение и цели

1.1. Назначение

Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.

Реализация: server.js — точка входа, подключает все роуты. ui/index.js — UI-слой.

1.2. Цели

  • Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.

    Реализация: ui/routes/entries.js — UI CRUD. src/api/routes/entries.js — REST API.

  • Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.

    Реализация: ui/routes/admin.js — /admin и /audit UI. src/api/routes/admin.js — API /companies, /audit.

  • Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.

    Реализация: src/routes/export.js — GET /export. src/validators.js строки 125–181 — функция aggregateCIDRs.

2. Объем работ

  • Web-страница / закладка в личном кабинете для управления whitelist-записями.

    Реализация: views/index.ejs — главная страница. ui/routes/entries.js — роуты GET /, POST /add, POST /edit/:id, POST /delete/:id.

  • Авторизация через существующий экземпляр Keycloak (OIDC).

    Реализация: src/auth.js строки 220–260 — верификация JWT RS256 токена Keycloak. Строка 245: чтение claim ClientID. Строка 251: определение роли isAdmin.

  • Валидация формы на стороне клиента и сервера.

    Реализация: src/validators.js строки 26–79 — серверная валидация validate(). views/index.ejs — атрибут pattern в поле ввода (клиентская).

  • Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.

    Реализация: src/routes/export.js строки 2764 — GET /export, Content-Type: text/plain, aggregateCIDRs → join('\n').

  • Хранение записей, журнал аудита.

    Реализация: sql/schema.sql — таблицы whitelist_entries, audit_log. src/queries.js строки 211–230 — функция logAudit.

  • Административное управление лимитами по компаниям.

    Реализация: src/api/routes/admin.js строки 2735 — PATCH /companies/:id/limit. src/queries.js — функция setLimit. views/admin.ejs — UI формы лимитов.

3. Роли и права доступа

Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.

Реализация: src/auth.js строка 31: ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'WZ01112'. Строка 251: isAdmin: clientId === ADMIN_CLIENT_ID.

Роль Идентификация Видимость записей Права на изменение
Клиент (client) clientId Только записи компаний, к которым принадлежит пользователь. Создание, редактирование и удаление записей своих компаний (в пределах лимита).
Администратор (admin) clientId = WZ01112 (Нубес) и отдельный чек-бокс Записи всех компаний. Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний.

Реализация ролей в API: src/api/routes/entries.js — функция resolveCompany(): если req.user.isAdmin && req.query.company → берёт чужую компанию, иначе getOrCreateCompany(clientId). src/auth.js строки 260265 — middleware requireAdmin.

3.1. Принадлежность к компании

Принадлежность пользователя к компании определяется через IAM API (GET /api/v1/auth/user). Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.

Реализация: src/auth.js — функция fetchIamUser(token) вызывает GET {IAM_API_URL}/api/v1/auth/user, получает profiles[] со всеми компаниями и userInfo с данными пользователя. userInfo.isAdmin — флаг админа (вместо хардкода WZ01112).

🔧 Требует доработки: fetchIamUser ещё не реализована. Сейчас данные берутся из JWT (payload.ClientID). Правильный источник — IAM API.

Ожидаемые данные из IAM API:

  • profiles[].client_id — все компании пользователя
  • profiles[].is_active_profile — активная компания
  • profiles[].id — ID профиля для POST /switch-profile
  • userInfo.clientID — WZ-код активной компании
  • userInfo.company — название компании
  • userInfo.companyId — UUID компании
  • userInfo.isAdmin — флаг администратора
  • userInfo.email — идентификация пользователя для аудита

4. Функциональные требования

4.1. Просмотр списка записей

  • Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.

    Реализация: ui/routes/entries.js — GET /: для admin грузит getAllCompanies() + фильтр по ?company=<id>. views/index.ejs — dropdown компаний для admin, таблица записей.

  • Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.

    Реализация: views/index.ejs строки 280–295 — колонки таблицы: value_cidr, comment, created_by, created_at, updated_at. Даты в timezone Europe/Moscow.

  • Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.

    Реализация: src/queries.js строки 2531 — listEntries(companyId, includeDeleted). SQL: AND deleted_at IS NULL когда includeDeleted=false.

    ⚠️ Фильтр "показать удалённые" в UI не реализованincludeDeleted всегда false.

  • Отображается текущее использование лимита: «использовано X из N».

    Реализация: src/api/routes/entries.js строка 62: res.json({ entries, limit, used: entries.length }). views/index.ejs — блок статистики <%= used %> / <%= limit %>.

4.2. Создание записи

  • Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).

    Реализация: views/index.ejs — форма POST /add с полями value и comment. Атрибут maxlength="18" на поле адреса.

  • Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.

    Реализация: src/validators.js строки 2679 — validate(input). Вызывается в src/queries.js строка 33: const { cidr, wasNormalized } = validate(rawValue).

  • Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.

    Реализация: src/queries.js строки 3270 — createEntry(): блокировка строки компании (FOR UPDATE), проверка лимита (строки 43–46), проверка дубликатов и пересечений (строки 49–57), проверка запрещённых диапазонов в validate() (строки 7375 validators.js).

  • При успешном сохранении создаётся запись аудита.

    Реализация: src/queries.js строка 66: logAudit(userEmail, companyId, 'CREATE', null, cidr, ...).

4.3. Редактирование записи

  • Редактирование значения и комментария доступно компании в рамках своих прав.

    Реализация: ui/routes/entries.js — POST /edit/:id. src/api/routes/entries.js — PATCH /entries/:id. Modal в views/index.ejs — кнопка .btn-edit, event delegation в <script>.

  • При изменении значения повторно выполняется полный набор проверок валидации и пересечений.

    Реализация: src/queries.js строки 77115 — updateEntry(): валидация через validate(), проверка пересечений (исключая саму запись: AND id <> $2).

  • Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.

    Реализация: src/queries.js строка 107: logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, ...).

4.4. Удаление записи (soft delete)

  • Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.

    Реализация: src/queries.js строки 118145 — deleteEntry(): UPDATE SET deleted_at = NOW(), deleted_by = userEmail. sql/schema.sql — колонки deleted_at, deleted_by в таблице whitelist_entries.

  • Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.

    Реализация: src/queries.js строка 50: COUNT считает только WHERE deleted_at IS NULL. src/routes/export.js — запрос только активных записей.

  • Действие фиксируется в журнале аудита.

    Реализация: src/queries.js строка 134: logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, ...).

4.5. Лимит записей на компанию

  • Действует глобальный лимит по умолчанию: 15 активных записей на компанию.

    Реализация: src/queries.js строки 1720 — getLimit(): parseInt(process.env.DEFAULT_LIMIT) || 15.

  • Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.

    Реализация: src/queries.js строка 18: process.env.DEFAULT_LIMIT — переменная окружения, не хардкод.

  • Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).

    Реализация: src/api/routes/admin.js строки 2735 — PATCH /companies/:id/limit. src/queries.js строка 20: company.custom_limit != null ? company.custom_limit : defaultLimit. sql/schema.sql — колонка custom_limit в таблице companies.

  • При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.

    Реализация: src/queries.js строки 4346: if (cnt >= limit) throw new Error('Лимит исчерпан: N из N'). API возвращает 409.

  • Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.

    Реализация: src/api/routes/admin.js строки 28–35 — setLimit просто записывает значение без удаления записей. Блокировка создания — через проверку cnt >= limit в createEntry.

4.6. Журнал аудита

  • Все изменяющие операции фиксируются неизменяемыми записями аудита.

    Реализация: src/queries.js строки 211218 — logAudit(): INSERT в audit_log без UPDATE/DELETE операций над ней.

  • Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.

    Реализация: sql/schema.sql — таблица audit_log: колонки user_email, created_at, company_id, action, old_value, new_value. views/audit.ejs строки 131–160 — отображение.

  • Журнал доступен для просмотра только администратору.

    Реализация: src/api/routes/admin.js — GET /audit защищён apiRequireAdmin. ui/routes/admin.js — GET /audit проверяет req.session.user.isAdmin.

4.7. Внешняя выдача агрегированного списка

  • Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.

    Реализация: src/validators.js строки 125181 — aggregateCIDRs(): сортировка, слияние перекрывающихся диапазонов, преобразование обратно в CIDR.

  • Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.

    Реализация: src/routes/export.js строки 2764 — GET /export. Content-Type: text/plain, ответ: aggregated.join('\n').

  • Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).

    ⚠️ Не реализовано — /export требует Bearer-токен (авторизован). По ТЗ должен быть доступен без авторизации по IP-списку.

5. Требования к валидации

Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.

Реализация: src/validators.js — вся серверная валидация. views/index.ejs — атрибут pattern (клиент).

Правило Описание Реализация
Формат IPv4 Допускается одиночный адрес или подсеть CIDR /22–/32. src/validators.js строки 41–53: добавление /32 если нет маски, проверка mask < 22 || mask > 32.
Только IPv4 IPv6-значения или доменные имена отклоняются. src/validators.js строки 3136: if (raw.includes(':')) → отклонить, проверка букв.
Корректность подсети Host-биты обнуляются, пользователь уведомляется о нормализации. src/validators.js строки 55–69: битовая арифметика, wasNormalized = addr !== networkAddr. Флаш-сообщение в ui/routes/entries.js.
Запрет серых адресов Диапазоны из Приложения А запрещены. src/validators.js строки 420: BLOCKED_RANGES[]. Строки 73–75: проверка overlaps().
Отсутствие дубликатов Совпадающие записи в компании запрещены. src/queries.js строка 53: if (row.value_cidr === cidr) throw. Уникальный индекс в sql/schema.sql.
Отсутствие пересечений Пересечение с существующей записью в компании запрещено. src/queries.js строки 49–57: перебор активных записей + overlaps(). Между компаниями — допускается.
Длина комментария Не более 255 символов. src/validators.js строка 183 (или в api/routes/entries.js): проверка comment.length > 255 → 400.

Приложение А – Список запрещённых к созданию подсетей

Реализация: src/validators.js строки 4–20 — массив BLOCKED_RANGES.

Назначение Префикс
Private (RFC1918) 10.0.0.0/8
Private (RFC1918) 172.16.0.0/12
Private (RFC1918) 192.168.0.0/16
CGNAT (RFC6598) 100.64.0.0/10
Loopback 127.0.0.0/8
Link-local (APIPA) 169.254.0.0/16
IANA special block 192.0.0.0/24
TEST-NET-1 (docs) 192.0.2.0/24
TEST-NET-2 (docs) 198.51.100.0/24
TEST-NET-3 (docs) 203.0.113.0/24
Benchmarking 198.18.0.0/15
Multicast 224.0.0.0/4
Reserved (Class E) 240.0.0.0/4
Limited broadcast 255.255.255.255/32

⚠️ Расхождения с ТЗ (что не реализовано)

Пункт ТЗ Статус
3.1 Несколько компаний для одного пользователя Не реализовано — ждём формат claim от devops
4.1 Фильтр soft-deleted записей для admin Не реализовано — includeDeleted всегда false
4.7 /export без авторизации (по IP) Не реализовано — требует Bearer-токен