21 KiB
Техническое задание Микросервис управления доверенными адресами клиентов 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: чтение claimClientID. Строка 251: определение ролиisAdmin. -
Валидация формы на стороне клиента и сервера.
Реализация:
src/validators.jsстроки 26–79 — серверная валидацияvalidate().views/index.ejs— атрибутpatternв поле ввода (клиентская). -
Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
Реализация:
src/routes/export.jsстроки 27–64 — 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строки 27–35 — 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строки 260–265 — middlewarerequireAdmin.
3.1. Принадлежность к компании
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
Реализация:
src/auth.jsстрока 245:clientId = payload.ClientID.src/queries.jsстроки 6–15:getOrCreateCompany(clientId, companyName)— атомарный upsert.⚠️ Сценарий нескольких компаний не реализован — в реальном токене Nubes
ClientID= одна строка, массива нет. Ждём уточнения от devops.
Ожидаемые claims (имена согласуются с командой Keycloak):
ClientID— идентификатор компании →src/auth.jsстрока 245email— идентификация пользователя для аудита →src/auth.jsстрока 248
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. Даты в timezoneEurope/Moscow. -
Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
Реализация:
src/queries.jsстроки 25–31 —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строки 26–79 —validate(input). Вызывается вsrc/queries.jsстрока 33:const { cidr, wasNormalized } = validate(rawValue). -
Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
Реализация:
src/queries.jsстроки 32–70 —createEntry(): блокировка строки компании (FOR UPDATE), проверка лимита (строки 43–46), проверка дубликатов и пересечений (строки 49–57), проверка запрещённых диапазонов вvalidate()(строки 73–75 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строки 77–115 —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строки 118–145 —deleteEntry(): UPDATE SETdeleted_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строки 17–20 —getLimit():parseInt(process.env.DEFAULT_LIMIT) || 15. -
Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
Реализация:
src/queries.jsстрока 18:process.env.DEFAULT_LIMIT— переменная окружения, не хардкод. -
Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
Реализация:
src/api/routes/admin.jsстроки 27–35 — 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строки 43–46: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строки 211–218 —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строки 125–181 —aggregateCIDRs(): сортировка, слияние перекрывающихся диапазонов, преобразование обратно в CIDR. -
Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
Реализация:
src/routes/export.jsстроки 27–64 — 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 строки 31–36: if (raw.includes(':')) → отклонить, проверка букв. |
| Корректность подсети | Host-биты обнуляются, пользователь уведомляется о нормализации. | src/validators.js строки 55–69: битовая арифметика, wasNormalized = addr !== networkAddr. Флаш-сообщение в ui/routes/entries.js. |
| Запрет серых адресов | Диапазоны из Приложения А запрещены. | src/validators.js строки 4–20: 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-токен |