docs: перенос документации из IPWhiteList + CONTEXT.md (резюме для нового чата)

This commit is contained in:
2026-05-30 19:10:34 +03:00
parent c85a438ba2
commit d31b8952b4
18 changed files with 34600 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
# Состояние проекта на 2026-05-30 14:43 (ветка `sonnet`)
## Репозитории
| Репо | URL | Ветка | Локальный путь |
|---|---|---|---|
| Код приложения | `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` | **sonnet** | `/home/naeel/ipwhitelist-app` |
| Документация | `https://gitea.services.ngcloud.ru/Nail/IPWhiteList.git` | main | `/home/naeel/IPWhiteList` |
## Стек
Node.js + Express + EJS + PostgreSQL + pg pool + jsonwebtoken
## Деплой
- URL: `https://white.nodejsk8s.dev.nubes.ru`
- DEV_MODE=true (мок-аутентификация)
- ⚠️ Нужен передеплой Nubes для ветки sonnet
## БД
- `write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru`
- user: `super`, db: `ipwhitelist`
- Миграция от 2026-05-30 применена (CHECK, UNIQUE, индексы)
---
## Что сделано в ветке `sonnet` (4 коммита поверх master)
### 1. `src/validators.js` — CIDR агрегация
- `aggregateCIDRs(cidrs)` — суммаризация: merge пересекающихся + смежных диапазонов → минимальный набор CIDR
- Вспомогательные: `numToIP(n)`, `rangeToCIDRs(start, end)`
- Экспортируется и используется в `/export`
### 2. `src/auth.js` — admin-роль
- `ADMIN_CLIENT_ID` = `process.env.ADMIN_CLIENT_ID || 'WZ01112'`
- `req.user.isAdmin` — определяется по `clientId === ADMIN_CLIENT_ID`
- `DEV_ADMIN=true` в `.env` → admin-права в dev-режиме
- `requireAdmin` middleware — 403 для не-admin
### 3. `src/queries.js` — новые функции
- `getCompanyById(id)` — компания по числовому PK
- `getAllCompanies()` — все компании + `active_count` (LEFT JOIN)
- `setLimit(companyId, newLimit)` — установить/сбросить (null) индивидуальный лимит
- `getAudit()` — обновлён: JOIN с companies (поля `company_name`, `client_id`)
### 4. `server.js` — новые роуты
- `/export` — публичный (до auth.middleware), возвращает агрегированный список всех компаний
- `GET /` — admin: видит все компании с переключателем `?company=X`
- `POST /edit/:id` — редактирование записи (admin + user)
- `GET /audit` — журнал аудита (только admin)
- `GET /admin` — управление лимитами (только admin)
- `POST /admin/limit/:companyId` — изменить/сбросить лимит компании
- `backUrl()` — хелпер для редиректа обратно с учётом контекста admin/user
### 5. `views/index.ejs`
- Admin-панель выбора компании (dropdown + быстрые ссылки на Аудит/Лимиты)
- Кнопки Аудит/Лимиты/Выйти в header для admin
- Кнопка "Изменить" в таблице (открывает edit modal)
- Edit modal — overlay с формой, закрывается по Escape/backdrop
- Скрытый `company_id` в формах add/delete для корректной admin-ветки
### 6. `views/audit.ejs` — **новый**
- Таблица журнала аудита с фильтром по компании
- Цветные badges (CREATE/UPDATE/DELETE)
- old_value → new_value стрелочкой
### 7. `views/admin.ejs` — **новый**
- Таблица всех компаний: client_id, active_count, лимит
- Прогресс-бар использования (зелёный/янтарный/красный)
- Форма изменения лимита с подтверждением; кнопка ↺ сброс на дефолт
---
## Не сделано (production-hardening, не баги)
- helmet (X-Frame-Options, CSP, HSTS)
- rate-limit на POST /add, /delete, /export
- CSRF-токены в формах
- JWT-верификация через внешний JWKS (нужен URL от девопсов)
- Multi-company (нужен формат claims от платформы — массив clientId?)
## Мёртвый код (не критично)
- `isSubnetOf()` в validators.js — определена, не используется, не экспортируется
## Для прода
Выставить в `.env`:
```
NODE_ENV=production
JWKS_URL=<auth-api JWKS URL>
ADMIN_CLIENT_ID=<clientId администратора>
DEFAULT_LIMIT=15
```
## Репозитории
| Репо | URL | Ветка | Локальный путь |
|---|---|---|---|
| Код приложения | `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` | master | `/home/naeel/ipwhitelist-app` |
| Документация | `https://gitea.services.ngcloud.ru/Nail/IPWhiteList.git` | main | `/home/naeel/IPWhiteList` |
## Стек
Node.js + Express + EJS + PostgreSQL + pg pool + jsonwebtoken
## Деплой
- URL: `https://white.nodejsk8s.dev.nubes.ru`
- DEV_MODE=true (мок-аутентификация)
- ⚠️ Код запушен, но Nubes не передеплоил — крутится старая версия
## БД
- `write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru`
- user: `super`, db: `ipwhitelist`
- Миграция от 2026-05-30 применена (CHECK, UNIQUE, индексы)
---
## Что сделано (запушено)
### 1. `src/validators.js` — исправлены 3 бага
- Запрещённые диапазоны: `isSubnetOf``overlaps` (обход через суперсеть `/22`)
- Маска: `parseInt('24abc')` глотал мусор → строгая проверка `/^\d{1,2}$/`
- Множественные слэши: `10.0.0.0/24/8` теперь отклоняется
### 2. `src/queries.js` — транзакции + гонки
- `createEntry`, `updateEntry`, `deleteEntry` — внутри транзакции с `SELECT ... FOR UPDATE`
- `getOrCreateCompany` — атомарный `INSERT ... ON CONFLICT`
- `getLimit``!= null` вместо `||` (custom_limit=0 не игнорируется)
- `logAudit` — принимает клиента транзакции (пишется атомарно)
- `getExportCIDRs` — фильтр по `companyId`
- `deleteEntry``company_id` в WHERE
### 3. `src/auth.js` — **новый.** Мок JWT-аутентификация
- Генерирует RSA-ключи при старте
- JWKS endpoint: `/.well-known/jwks.json`
- `verifyJWT(token)` — RS256, issuer: `mock-auth-api`
- `issueJWT(claims)` — выпускает токен с claims как в HAR (`ClientID`, `company_id`, `company_name`, `email`)
- Middleware: извлекает JWT из cookie (`jwt`) или `Authorization: Bearer`
- DEV_MODE: при `DEV_MODE=true && NODE_ENV!=production` — обход auth
- **Для прода:** выставить `JWKS_URL=https://auth-api.../jwks` → switches to external verification
### 4. `views/login.ejs` — **новый.** Мок-страница входа
- Выбор из 3 пользователей (admin WZ01112, тест WZ01325, компания 2 WZ02001)
- В проде заменяется на редирект в Keycloak
### 5. `server.js`
- `cookie-parser` для чтения JWT из cookie
- `/healthz` — выше auth (k8s probe)
- `/login` GET/POST — мок-логин
- `/logout` — чистит cookie
- `/export` — только для своей компании (с авторизацией)
- `req.query.error` читается
- `urlencoded({ limit: '32kb' })`
### 6. `sql/schema.sql`
- UNIQUE INDEX на активный `(company_id, value_cidr)` WHERE deleted_at IS NULL
- CHECK на `value_cidr` формат
- CHECK на `audit_log.action IN ('CREATE','UPDATE','DELETE')`
- CHECK на `custom_limit IS NULL OR >= 0`
- FK: `ON DELETE RESTRICT`
- Составной индекс `(company_id, created_at DESC)` на audit_log
- Индекс `(company_id, created_at DESC)` на whitelist_entries
### 7. `views/index.ejs`
- `pattern` + `maxlength="18"` + `title` на инпуте value
---
## Ревью (`/home/naeel/IPWhiteList/research/`)
| Файл | Что |
|---|---|
| `REVIEW-SUMMARY.md` | Сводка всех находок (11 критических, 21 средний) |
| `opus-review-validators.md` | 1 критичный + 2 средних |
| `opus-review-queries.md` | 3 гонки + audit + getLimit |
| `opus-review-server.md` | JWT без подписи, 401, CSRF, /export |
| `opus-review-schema.md` | UNIQUE, CIDR, FK, индексы |
| `opus-review-ejs.md` | CSRF, clickjacking, client-валидация |
| `auth-flow.md` | Анализ HAR: claims, цепочка auth-api |
---
## Не сделано (production-hardening, не баги)
- helmet (X-Frame-Options, CSP, HSTS)
- rate-limit на POST /add, /delete, /export
- CSRF-токены в формах
- JWT-верификация через внешний JWKS (нужен URL от девопсов)
- Admin-признак в токене (нужен пример токена админа)
- Multi-company (нужен формат claims от платформы)
## Для прода
Выставить в `.env`:
```
NODE_ENV=production
JWKS_URL=<auth-api JWKS URL>
```
Всё остальное работает без изменений.
+156
View File
@@ -0,0 +1,156 @@
Техническое задание
Микросервис управления доверенными адресами клиентов
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
───────────────────────────────────────────────────────────────
КРАТКОЕ ОПИСАНИЕ (пояснение к реализации)
───────────────────────────────────────────────────────────────
Сервис даёт клиентам облачного провайдера личный кабинет, где они сами указывают
свои доверенные IPv4-адреса и подсети. Эти адреса провайдер не блокирует во время
DDoS-атак — так легитимный трафик клиента не попадает под ложные срабатывания
фильтрации.
Что реализуется:
Личный кабинет клиента. Клиент входит через привычную авторизацию (Keycloak),
видит свой список доверенных адресов и управляет им сам: добавляет, редактирует,
удаляет записи с комментариями. Если пользователь работает с несколькими
компаниями — переключается между ними.
Проверка вводимых данных. Форма принимает только корректные IPv4-адреса и подсети,
отклоняет «серые» и служебные диапазоны, не допускает дубликатов и пересечений
внутри одной компании.
Ограничение по количеству. На компанию по умолчанию 15 записей. Лимит
настраивается глобально, а для отдельной компании администратор может поднять или
опустить его индивидуально.
Режим администратора (сетевые инженеры провайдера). Единое окно, где видны записи
всех компаний, с фильтрами и доступом к истории изменений. Администратор управляет
лимитами и при необходимости любыми записями.
История изменений (аудит). Каждое создание, изменение и удаление фиксируется: кто,
когда, что именно изменил. Удаление — логическое, данные физически сохраняются.
Выдача для систем фильтрации. Отдельный адрес, по которому системы защиты
автоматически забирают итоговый сводный список всех доверенных адресов (одним
txt-файлом, по строке на запись). Адреса при этом схлопываются в компактный общий
перечень.
───────────────────────────────────────────────────────────────
1. Назначение и цели
1.1. Назначение
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
1.2. Цели
Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
2. Объем работ
Web-страница / закладка в личном кабинете для управления whitelist-записями.
Авторизация через существующий экземпляр Keycloak (OIDC).
Валидация формы на стороне клиента и сервера.
Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
Хранение записей, журнал аудита.
Административное управление лимитами по компаниям.
3. Роли и права доступа
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
Роль
Идентификация
Видимость записей
Права на изменение
Клиент (client)
clientId
Только записи компаний, к которым принадлежит пользователь.
Создание, редактирование и удаление записей своих компаний (в пределах лимита).
Администратор (admin)
clientId = WZ01112 (Нубес) и отдельный чек-бокс
Записи всех компаний.
Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний.
3.1. Принадлежность к компании
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
Ожидаемые claims (имена согласуются с командой Keycloak):
clientID — идентификатор компании
email — идентификация пользователя для аудита
4. Функциональные требования
4.1. Просмотр списка записей
Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
Отображается текущее использование лимита: «использовано X из N».
4.2. Создание записи
Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
При успешном сохранении создаётся запись аудита.
4.3. Редактирование записи
Редактирование значения и комментария доступно компании в рамках своих прав.
При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
4.4. Удаление записи (soft delete)
Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
Действие фиксируется в журнале аудита.
4.5. Лимит записей на компанию
Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
4.6. Журнал аудита
Все изменяющие операции фиксируются неизменяемыми записями аудита.
Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
Журнал доступен для просмотра только администратору.
4.7. Внешняя выдача агрегированного списка
Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
5. Требования к валидации
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
Правило
Описание
Формат IPv4
Допускается одиночный адрес (например 203.0.113.10) или подсеть в нотации CIDR (например 203.0.113.0/24). Допускается использование масок /32 - /22. Маска /21 и больше не допускается.
Только IPv4
IPv6-значения или доменные имена отклоняются.
Корректность подсети
Введенный адрес с маской подсети должен нормализоваться к адресу подсети, все host-биты должны быть обнулены.
Пользователь должен быть уведомлен, что ввел адрес из хостовой части, а не адрес подсети и произошла нормализация.
Запрет серых адресов
Адреса и подсети из частных диапазонов (Приложение А) запрещены к добавлению.
Отсутствие дубликатов
В пределах одной компании запрещены полностью совпадающие записи.
Отсутствие пересечений
В пределах одной компании запрещено добавление записи, пересекающейся с уже существующей (включая вложенность подсетей). Между разными компаниями пересечения допускаются.
Длина комментария
Не более 255 символов; поле необязательное.
Приложение А – Список запрещенных к созданию подсетей.
Назначение
Префикс
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
+112
View File
@@ -0,0 +1,112 @@
# Дизайн-система платформы Nubes
> Извлечено из сохранённой страницы `h.h` (deck-test.ngcloud.ru)
---
## Сетка и контейнеры
| Элемент | Класс | Описание |
|---|---|---|
| Страница | `navigation:flex`, `services:grid` | Flex/grid на всём |
| Карточка | `ui-kit:bg-card ui-kit:rounded-xl ui-kit:border ui-kit:shadow-sm` | Белая карточка, border-radius 12px |
| Заголовок карточки | `ui-kit:bg-brand-grey-light ui-kit:px-3 ui-kit:py-3` | Серый фон `#f3f4f6`, padding 12px |
| Тело карточки | `ui-kit:px-3 ui-kit:py-3` | padding 12px |
---
## Цвета (переменные)
| Переменная | Назначение | Примерный HEX |
|---|---|---|
| `--brand-primary` | Основной цвет | `#2563eb` (синий) |
| `--brand-gray` | Цвет границ | `#d1d5db` |
| `--brand-grey-light` | Фон заголовков | `#f3f4f6` |
| `--brand-primary-dark` | Ховер ссылок | `#1d4ed8` |
---
## Формы
```
form-table (класс services:):
grid-template-columns: fit-content(200px) minmax(200px, 1fr) 0px
gap: 8px 16px
```
| Элемент | Класс | Стиль |
|---|---|---|
| Лейбл | `ui-kit:text-sm ui-kit:leading-none ui-kit:font-normal ui-kit:mb-1 ui-kit:ml-1` | 14px, sans-serif, отступ слева |
| Инпут | `ui-kit:h-9 ui-kit:rounded-md ui-kit:border ui-kit:px-3 ui-kit:text-sm` | h=36px, border, padding |
| Текстареа | `ui-kit:rounded-md ui-kit:border ui-kit:px-3 ui-kit:py-2 ui-kit:text-sm` | авто-height |
---
## Кнопки
| Тип | Стиль | Класс |
|---|---|---|
| Обычная | border, bg-white, hover:bg-accent | `ui-kit:border ui-kit:bg-background ui-kit:h-8 ui-kit:rounded-md` |
| Удалить | bg-destructive, text-white | `ui-kit:bg-destructive ui-kit:text-white` |
| Иконка | size-5 | `ui-kit:size-5 ui-kit:cursor-pointer` |
Все кнопки: `ui-kit:h-8 ui-kit:rounded-md ui-kit:gap-1.5 ui-kit:px-3`, 14px шрифт.
---
## Таблицы
| Элемент | Стиль |
|---|---|
| Обёртка | `ui-kit:rounded-md ui-kit:border ui-kit:overflow-hidden` |
| Шапка (th) | `ui-kit:bg-brand-grey-light`, uppercase, `ui-kit:py-1 ui-kit:px-2`, border-right |
| Ячейка (td) | `ui-kit:px-2 ui-kit:py-1`, border-right, border-bottom |
| Строка (tr) | `ui-kit:border-b ui-kit:border-brand-gray-8`, hover: `ui-kit:bg-brand-grey-light/50` |
---
## Иконки
Используются: **Lucide** (`lucide-*`)
Часто используемые:
- `lucide-square-pen` — редактировать
- `lucide-rotate-cw` — перезапустить
- `lucide-trash` — удалить
- `lucide-pause` — остановить
- `lucide-play` — запустить
- `lucide-save` — сохранить
- `lucide-x` — закрыть
- `lucide-copy` — копировать
- `lucide-check` — успех (зелёный)
- `lucide-x` — ошибка (красный)
---
## Типографика
- Основной шрифт: system-ui (Segoe UI, Roboto, etc.)
- Размер: `text-sm` = 14px, `text-base` = 16px
- Межстрочный: `leading-none` (1), `leading-normal` (1.5)
- Цвет текста: `#1a1a1a`
- Muted: `text-muted-foreground` = серый `#6b7280`
- Заголовки карточек: `font-semibold`, 16px
---
## Навигация (слева)
- Ширина: `var(--sidebar-width)` = 12rem (192px)
- Свёрнуто: `var(--sidebar-width-icon)` = 4.5rem (72px)
- Верхняя панель: h-20 (80px), border-t-4 border-t-blue-500
- Лого: инлайн SVG, 150px ширина
---
## Состояния
| Статус | Цвет |
|---|---|
| Успех / running | `text-green-500` + иконка `lucide-check` |
| Ошибка | `text-red-500` + иконка `lucide-x` |
| Предупреждение | `text-amber-500` |