Files
ipwhitelist-app/docs/legacy-ТЗ-реализация.md
T
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

243 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Техническое задание
Микросервис управления доверенными адресами клиентов
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` строки 125181 — функция `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` строки 211230 — функция `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. Принадлежность к компании
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
> **Реализация:** `src/auth.js` строка 245: `clientId = payload.ClientID`. `src/queries.js` строки 615: `getOrCreateCompany(clientId, companyName)` — атомарный upsert.
>
> ⚠️ **Сценарий нескольких компаний не реализован** — в реальном токене Nubes `ClientID` = одна строка, массива нет. Ждём уточнения от devops.
Ожидаемые claims (имена согласуются с командой Keycloak):
- `ClientID` — идентификатор компании → `src/auth.js` строка 245
- `email` — идентификация пользователя для аудита → `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`. Даты в 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` строки 2835 — 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` строки 420 — массив `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-токен |