docs: перенос всей документации в ipwhitelist-app

This commit is contained in:
“Naeel”
2026-05-30 19:11:36 +03:00
parent 53d3901239
commit 21eea523d5
33 changed files with 0 additions and 67937 deletions
-204
View File
@@ -1,204 +0,0 @@
# Состояние проекта на 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>
```
Всё остальное работает без изменений.
BIN
View File
Binary file not shown.
-156
View File
@@ -1,156 +0,0 @@
Техническое задание
Микросервис управления доверенными адресами клиентов
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
@@ -1,112 +0,0 @@
# Анализ проекта IP WhiteList — 30.05.2026
## Что сделано
| Компонент | Файл | Строк | Статус |
|---|---|---|---|
| Express-сервер, роуты | `server.js` | 101 | ✅ |
| Валидатор IPv4/CIDR | `src/validators.js` | 99 | ✅ |
| CRUD + audit_log | `src/queries.js` | 149 | ✅ |
| Пул PG | `src/db.js` | 24 | ✅ |
| UI (EJS, стиль Nubes) | `views/index.ejs` | 248 | ✅ |
| Схема БД | `sql/schema.sql` | 39 | ✅ |
| Favicon | `public/favicon.png` | — | ✅ |
| Дизайн-система (документ) | `docs/nubes-design-system.md` | — | ✅ |
| Тестирование | `tests/test-results.md` | 16/16 ✅ | ✅ |
### Валидатор — что проверяет:
- RFC1918 (10/8, 172.16/12, 192.168/16) — запрет
- TEST-NET (192.0.2/24, 198.51.100/24, 203.0.113/24) — запрет
- Loopback, link-local — запрет
- IPv6 — запрет
- Маска: допускается только /22–/32
- Нормализация host-битов (10.0.0.1/24 → 10.0.0.0/24)
- Пересечения с уже существующими записями — запрет
- Лимит записей на компанию (по умолчанию 15)
### Экспорт:
- `GET /export` → plain text, один CIDR на строку, без заголовков
---
## Что не готово (критично для продакшена)
### 1. JWT-авторизация
**Сейчас:** `DEV_MODE=true` — авторизация заглушена, `clientId` захардкожен.
**Нужно:** Распаковка JWT из `Authorization: Bearer <token>` → извлечение `clientId` и `email`.
Схема описана в `docs/auth-architecture.md`.
**Риск:** без этого нельзя открывать сервис для реальных пользователей — любой запрос видит и меняет все данные.
### 2. Изоляция данных по компании
**Сейчас:** Все роуты используют один захардкоженный `clientId`. БД правильная (таблица `companies` есть), но `WHERE company_id = $1` не работает по-настоящему без JWT.
**Нужно:** После JWT — всё заработает автоматически, код менять не придётся.
### 3. CSRF-защита на POST-формах
**Сейчас:** Формы без CSRF-токена.
**Нужно:** `csurf` middleware или `SameSite=Strict` на session cookie.
Или, если фронт перейдёт на fetch/JSON API — Authorization header автоматически решает проблему.
---
## Замечания и предложения
### Архитектура
- `server.js` содержит все роуты в одном файле (101 строка). Пока норм, но при добавлении админки/API станет неудобно. Рекомендую разбить на `routes/user.js`, `routes/admin.js`, `routes/api.js` перед тем как добавлять функциональность.
- `queries.js` возвращает сырые строки, не объекты с явным типом. Если сервис будет расти — стоит обернуть в Result-паттерн `{ ok, data, error }`.
### UI
- Нет пагинации. При лимите 15 записей на компанию — не критично. Если лимит поднимут до 100+ — нужна.
- Нет поиска/фильтрации в таблице.
- Алерты исчезают только при перезагрузке страницы (flash-сообщения). Если добавить JS — можно auto-dismiss через 5 секунд.
### Экспорт
- Сейчас `GET /export` отдаёт все CIDR без суммаризации. Если у компании 10 записей типа `1.2.3.0/28` и `1.2.3.16/28` — они будут двумя строками. Суммаризация в `/28` + `/28``/27` сократила бы файл и упростила настройку оборудования. Это отдельная задача, потребует библиотеку CIDR-merge.
### Тесты
- Тесты ручные (curl в md-файле). Для CI/CD нужен автоматический прогон: `jest` или `supertest`. Команда для инициализации: `npm install --save-dev jest supertest`.
### БД
- `audit_log` пишется, но нигде не отображается пользователю. Для полноты — стоит добавить вкладку "История" или endpoint `GET /audit`.
- Нет индекса на `whitelist_entries(company_id)` — при росте данных будет полный скан. Добавить в `schema.sql`:
```sql
CREATE INDEX idx_whitelist_company ON whitelist_entries(company_id);
CREATE INDEX idx_audit_company ON audit_log(company_id);
```
### Безопасность
- `.env` в `.gitignore` ✅ — правильно.
- `secrets.txt` в корне репо — нужно убедиться что он тоже в `.gitignore`.
- Параметризованные запросы в `queries.js` ✅ — SQL-инъекции закрыты.
- `express-validator` не подключён — валидация только на уровне `validators.js`. Нормально, т.к. все входные данные проходят через него.
---
## Приоритеты следующих задач
1. **JWT** — без этого продакшен не открыть
2. **CSRF** — параллельно с JWT
3. **Индексы БД** — 2 строки в schema.sql, риск нулевой
4. **Разбивка роутов** — перед добавлением админки
5. **Автотесты** — перед CI/CD
6. **Суммаризация CIDR** — nice to have
7. **Пагинация + фильтр** — после поднятия лимита
8. **Audit UI** — опционально
---
## Текущий стек
- Node.js + Express.js
- PostgreSQL (pg pool)
- EJS templates
- Платформа: Nubes NodeJS instance "white"
- URL: `https://white.nodejsk8s.dev.nubes.ru`
- Repo (код): `gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git`
- Repo (docs): `gitea.services.ngcloud.ru/Nail/IPWhiteList.git`
-95
View File
@@ -1,95 +0,0 @@
# Архитектура авторизации облачного портала
> ✅ ПРОВЕРЕНО: API успешно вызван через curl 2026-05-29.
---
## Схема аутентификации
```
Пользователь (браузер)
Keycloak (keycloak.nubes.ru, realm=cloud)
│ Authorization Code Flow
│ client_id=deck.ngcloud.ru
│ scope=openid email
auth-api.ngcloud.ru (СОБСТВЕННЫЙ сервис)
│ Создаёт свой JWT (issuer="auth-api")
│ Подпись: RS256 (RSA)
deck.ngcloud.ru (портал)
│ Микро-фронтенды (SPA): dashboard, contracts, services, ...
│ JWT хранится в localStorage: authApiTokens.access_token
IPWhiteList (наш сервис) ← будет встроен как микро-фронтенд в портал
```
## Важное
- JWT подписывает **не Keycloak**, а **auth-api**
- Issuer: `"auth-api"`, алгоритм: `RS256`
- Для валидации нужен публичный ключ auth-api (JWKS или статический)
---
## Структура JWT (access_token)
_Из localStorage → authApiTokens → access_token_
| Поле | Тип | Значение (пример) | Назначение |
|---|---|---|---|
| `iss` | string | `"auth-api"` | Кто выпустил токен |
| `sub` | string | `"0199e325-..."` | UUID пользователя |
| `iat` | number | `1780073927` | Выпущен (Unix time) |
| `exp` | number | `1780117127` | Истекает (~12 часов) |
| `jti` | string | `"4d8d7240-..."` | Уникальный ID токена |
| `ClientID` | string | `"WZ01325"` | ID **ТЕКУЩЕЙ** компании пользователя |
| `company_id` | string (UUID) | `"3e64aac6-..."` | UUID компании |
| `company_name` | string | `"Тест"` | Название компании |
| `email` | string | `"tazet@narod.ru"` | Email (для аудита) |
| `login` | string | `"tazet@narod.ru"` | Логин |
| `firstname` | string | `"Наиль"` | Имя |
| `lastname` | string | `"Тазетдинов"` | Фамилия |
| `token_type` | string | `"access"` | Тип токена |
## Что НЕ в JWT (отдельный authData в localStorage)
_Из localStorage → authData → v_
| Поле | Значение | Где используется |
|---|---|---|
| `userInfo.isAdmin` | `false` (boolean) | ⚠️ Признак администратора |
| `profiles[]` | Массив `{company_id, company_name, is_active_profile}` | Список всех компаний пользователя |
| `roles[]` | Массив `{role_id, role_name}` | Роли пользователя |
| `permissions.can_write` | `false` | Есть ли права на запись |
---
## Открытые вопросы (нужно уточнить с командой портала)
1. **Валидация JWT.** ✅ Выяснено: API gateway (`lk-api-gateway.ngcloud.ru`) сам валидирует JWT через auth-api. Нам достаточно передавать `Authorization: Bearer <JWT>`.
2. **isAdmin.** Флаг админа есть только в authData localStorage, но не в JWT. Как наш сервис узнает что пользователь — админ?
- Нужно уточнить: можно ли добавить `isAdmin` в JWT или получать через API gateway
3. **Список компаний.** В JWT — только одна компания. В authData.profiles — массив. Для переключателя компаний — откуда брать список?
4. **Монтирование в API gateway.** Наш сервис будет за `lk-api-gateway.ngcloud.ru` как отдельный route (например `/api/v1/whitelist/...`). Нужно уточнить процедуру добавления нового route.
---
## Как вызывать API (проверено curl'ом)
```bash
curl -H "Authorization: Bearer <JWT>" \
-H "Origin: https://deck.ngcloud.ru" \
-H "Cookie: __ddg1_=...; __ddg8_=...; __ddg9_=...; __ddg10_=..." \
"https://lk-api-gateway.ngcloud.ru/api/v1/..."
```
- **API Gateway:** `lk-api-gateway.ngcloud.ru` (не deck-api.ngcloud.ru!)
- **DDOS-Guard cookies ОБЯЗАТЕЛЬНЫ** (без них 403)
- **JWT issuer:** `auth-api`
- **Алгоритм:** RS256
-131
View File
@@ -1,131 +0,0 @@
# План разработки IP WhiteList — актуальный (30.05.2026)
> Основан на ТЗ (`WhiteIPlist.txt`) и реальном состоянии кода.
> Все предыдущие планы (plan.md, plan-v2.md, plan-working.md, plan-gemini.md, plan-gpt54.md) устарели.
---
## Что уже работает (не трогать)
| Компонент | Файл | Состояние |
|---|---|---|
| Валидатор IPv4/CIDR | `src/validators.js` | ✅ Полный — все 14 диапазонов Приложения А |
| CRUD функции | `src/queries.js` | ✅ `createEntry`, `updateEntry`, `deleteEntry`, `listEntries`, `getExportCIDRs`, `getAudit` |
| Soft delete | `src/queries.js` + `sql/schema.sql` | ✅ `deleted_at`, `deleted_by` |
| Схема БД | `sql/schema.sql` | ✅ Все таблицы и индексы |
| Аудит-лог запись | `src/queries.js` | ✅ Пишется при CREATE/UPDATE/DELETE |
| UI главная страница | `views/index.ejs` | ✅ Стиль Nubes |
| Экспорт txt | `server.js GET /export` | ✅ Работает (без суммаризации) |
| Лимит + custom_limit | `src/queries.js` | ✅ |
---
## Что есть в queries.js, но не подключено к роутам
- `updateEntry` — написана, роута `POST /update/:id` нет, UI-кнопки нет
- `getAudit` — написана, роута и страницы нет
- `listEntries(companyId, includeDeleted=true)` — параметр есть, но нигде не вызывается с `true`
---
## Что отсутствует полностью
### A. Роуты (server.js)
- `POST /update/:id` — редактирование записи
- `GET /admin` — страница администратора
- `POST /admin/limit/:companyId` — изменение лимита компании
- `GET /admin/audit` — журнал аудита (только для admin)
### B. UI (views/)
- Кнопка и inline-форма редактирования в таблице
- Страница admin (`views/admin.ejs`):
- Список всех компаний
- Записи с фильтром по компании
- Переключатель "показать удалённые"
- Журнал аудита
- Изменение лимита компании
### C. Авторизация
- Текущий код: наивный base64 decode JWT — не проверяет подпись, не Keycloak
- Нужно: `openid-client` (OIDC), интеграция с Keycloak, проверка `clientID` и `email` из claims
- Admin-роль: `clientId === 'WZ01112'` — определяется из claim
### D. Суммаризация CIDR в экспорте
- ТЗ требует агрегацию в минимальный набор CIDR
- Нужна библиотека: `npm install cidr-tools` или `ip-range-check`
- Или самописный merge (sorted ranges → contiguous merge)
### E. Клиентская валидация
- ТЗ: валидация на клиенте обязательна, серверная — авторитетная
- Сейчас: только серверная
- Нужно: JS в index.ejs — проверить формат CIDR, маску /22/32, не RFC1918
### F. Переключатель компаний
- ТЗ: пользователь может быть в нескольких компаниях (несколько `clientId` в токене)
- Нужно: UI-dropdown, переключение активного `clientId`, хранение выбора в сессии
---
## Обнаруженные баги
### overlaps() в validators.js — логическая ошибка
```js
// Текущий код (неверно):
return a.start <= b.end && b.start <= a.start ||
b.start <= a.end && a.start <= b.start;
// Правильно:
return a.start <= b.end && b.start <= a.end;
```
Стандартное условие пересечения отрезков. Текущий код даёт false positive для некоторых смежных диапазонов.
**Приоритет: высокий** — влияет на корректность проверки пересечений.
---
## Порядок реализации
### Этап 1 — Критические фиксы (не ломают работу)
1. Исправить `overlaps()` в `validators.js`
2. Добавить `POST /update/:id` в `server.js`
3. Добавить кнопку редактирования + inline-форму в `views/index.ejs`
### Этап 2 — Полнота ТЗ
4. Клиентская валидация (JS в index.ejs)
5. Суммаризация CIDR в `GET /export` (cidr-tools или ручной merge)
6. Определение admin-роли (`clientId === 'WZ01112'`)
7. Страница `/admin` + роут `GET /admin`
8. Роут `POST /admin/limit/:companyId`
9. Журнал аудита для admin (`GET /admin/audit`)
### Этап 3 — Авторизация (блокирующая для продакшена)
10. `npm install openid-client` — OIDC Keycloak
11. Заменить auth middleware на реальную проверку токена
12. Убрать `DEV_MODE` (или оставить только для локальной разработки)
### Этап 4 — Опционально
13. Переключатель компаний (multi-company claim)
14. Автотесты: `npm install --save-dev jest supertest`
15. Пагинация (только если лимит вырастет > 50)
16. Audit UI для клиента (не только admin)
---
## Зависимости
```
openid-client — OIDC / Keycloak
cidr-tools — суммаризация CIDR в экспорте
```
Всё остальное — в рамках существующего стека (Express, EJS, pg).
---
## Что НЕ нужно делать
- Переписывать validators.js (только исправить overlaps)
- Переписывать queries.js — всё уже написано
- Менять схему БД — она полная
- Пересобирать стек — Express + EJS достаточно для ТЗ
-55
View File
@@ -1,55 +0,0 @@
# Актуальный план разработки — IP WhiteList Microservice
> **Автор:** GitHub Copilot (Gemini 3.1 Pro Preview)
> **Дата:** 2026-05-30
> **Основание:** ТЗ (WhiteIPlist.txt) + Реальный код (Node.js/Express)
## 1. Анализ предыдущего плана и моё мнение
Мой предыдущий план (`plan-gemini.md`) оказался **полностью оторванным от реальности**. Я предполагал писать всё с нуля на Python/FastAPI+SQLModel. На деле же ядро полностью готово и написано на **Node.js, Express, EJS и чистом SQL (pg pool)**.
Писать с нуля на питоне — это плодить техдолг и выкидывать рабочий код. Более того, серверная валидация, структура БД, CRUD и UI уже в целом соответствуют ТЗ, но сильно не хватает связующих звеньев.
Поэтому мой главный тезис: **хватит придумывать архитектуру, нужно закрывать дыры по функциональным требованиям ТЗ в текущем Node.js проекте.**
## 2. Разрыв между ТЗ (WhiteIPlist.txt) и кодом (Node.js)
Что готово и работает:
- **База данных:** Полная структура (`companies`, `whitelist_entries`, `audit_log`), реализован soft delete (`deleted_at`).
- **Слой данных (`queries.js`):** Работает базовый CRUD, сохраняются логи аудита, обрабатываются ограничения (глобальные и кастомные).
- **Валидация (`validators.js`):** Реализованы проверки на IPv4, маски /22-/32, зашиты все запрещённые диапазоны (RFC1918, CGNAT, Loopback и т.д.).
- **UI:** Причесанный EJS-шаблон добавления/просмотра/удаления.
Чего не хватает по ТЗ (Фокус дальнейшей разработки):
1. **Авторизация (Keycloak OIDC):** Сейчас сделан временный парсинг JWT через Base64 без валидации ключей (DEV_MODE).
2. **Multi-company и Роли (Админ/Клиент):** Не реализованы переключатель компаний и админские страницы (видимость всех записей, изменение лимитов, просмотр аудита).
3. **Редактирование записей:** Метод `updateEntry` написан в БД-слое, но UI и роут отсутствуют. Формально ТЗ не закрыто.
4. **Агрегация в Экспорте:** Маршрут `GET /export` просто выплёвывает адреса в столбик, тогда как ТЗ жёстко требует суммаризировать подсети в минимальный набор CIDR.
5. **Клиентская валидация:** ТЗ явно требует валидировать формат и маски на фронтенде перед отправкой.
## 3. Детальный план по шагам (Node.js)
### Этап 1: Исправление багов в текущем MVP
- [ ] **Баг с overlaps:** В `validators.js` функция `overlaps()` содержит логическую ошибку в `start`/`end` (может пропускать пересекающиеся подсети). Переписать условие.
- [ ] **UI Редактирования:** Добавить роут `POST /update/:id` в `server.js` и добавить кнопку/форму "Изменить" в `index.ejs`, подключив существующую `updateEntry(...)`.
### Этап 2: Строгое соответствие ТЗ (Валидация и Экспорт)
- [ ] **Суммаризация CIDR:** Так как в Node.js нет `ipaddress.collapse_addresses`, необходимо использовать библиотеку вроде `cidr-tools` (функция `merge()` отлично справится) для `GET /export`.
- [ ] **Клиентская валидация:** Добавить минимальный JavaScript в `index.ejs` для проверки валидности вводимого IP-адреса и маски до ухода POST-запроса, чтобы экономить серверные ресурсы (Требование ТЗ).
### Этап 3: Подключение OIDC Keycloak
- [ ] Установка библиотеки `openid-client` (или `passport-openidconnect`).
- [ ] Настройка middleware: получение сертификатов из Keycloak, валидация подписи JWT-токена.
- [ ] Извлечение claim `clientID` (и логика переключения между компаниями, если `clientID` является массивом). Определение роли (кто админ, `WZ01112`). Обязательный отказ от dev-заглушки в PROD.
### Этап 4: Админ-панель (пользователь WZ01112)
- [ ] Роут `GET /admin` и шаблон `admin.ejs`. Таблица со всеми компаниями и их лимитами, фильтрацией.
- [ ] Функционал установки `custom_limit` для компании.
- [ ] Страница/вкладка `GET /admin/audit` для просмотра `audit_log`.
- [ ] Переключатель отображения Soft-deleted записей.
## 4. Зависимости
В `package.json` придется добавить лишь две новые production-зависимости, не усложняя проект:
- `cidr-tools` (для агрегации при экспорте)
- `openid-client` / `jsonwebtoken` (для безопасной работы с Keycloak)
Всё остальное будет реализовано в рамках существующего стека.
-552
View File
@@ -1,552 +0,0 @@
# Актуальный план реализации — IP WhiteList
> Автор: GitHub Copilot (GPT-5.4)
> Дата: 2026-05-30
> Основание: ТЗ из WhiteIPlist.txt + фактический код в ipwhitelist-app
> Статус: заменяет предыдущую версию плана
## 1. Вывод по текущему состоянию
Предыдущий план был неполным, потому что строился не от ТЗ, а от видимого MVP. После сверки с WhiteIPlist.txt картина такая:
1. Основа сервиса уже написана лучше, чем казалось: схема БД, soft delete, аудит, custom_limit, updateEntry и полный список запрещённых диапазонов уже есть в коде.
2. Основной разрыв находится не в модели данных, а между слоями: часть функций реализована в src/queries.js, но не выведена в server.js и views.
3. Главные недостающие вещи для соответствия ТЗ: нормальная OIDC-авторизация, admin-сценарии, редактирование из UI, клиентская валидация, агрегирующий экспорт.
4. Переписывать стек или БД не нужно. Нужно довести существующую реализацию до полноты требований.
## 2. Что требует ТЗ
Сервис по ТЗ обязан поддерживать:
1. Self-service страницу управления whitelist-записями.
2. Авторизацию через Keycloak OIDC.
3. Роли client и admin.
4. Возможность работы пользователя с одной или несколькими компаниями.
5. Создание, редактирование и soft delete записей.
6. Серверную и клиентскую валидацию IPv4/CIDR.
7. Глобальный лимит и индивидуальные лимиты по компаниям.
8. Журнал аудита.
9. Внешний endpoint выдачи агрегированного списка активных CIDR.
10. Admin-функции: просмотр всех компаний, фильтрация, просмотр удалённых записей, изменение лимитов.
## 3. Что уже реализовано сейчас
### 3.1 База данных
В sql/schema.sql уже есть всё базовое, что нужно для ТЗ:
1. Таблица companies с client_id и custom_limit.
2. Таблица whitelist_entries с updated_by, updated_at, deleted_by, deleted_at.
3. Таблица audit_log.
4. Индекс активных записей и индекс по audit_log(company_id).
Вывод: схему БД переписывать не нужно.
### 3.2 Серверная логика
В src/queries.js уже реализованы:
1. getOrCreateCompany
2. getLimit
3. listEntries с includeDeleted
4. createEntry
5. updateEntry
6. deleteEntry как soft delete
7. getExportCIDRs
8. getAudit
Вывод: слой работы с БД уже покрывает значительную часть ТЗ.
### 3.3 Валидация
В src/validators.js уже есть:
1. Только IPv4.
2. Маски только /22–/32.
3. Нормализация host-битов.
4. Запрет всех диапазонов из Приложения А ТЗ, включая CGNAT, Benchmarking, Multicast, Reserved и Limited broadcast.
5. Проверка пересечений.
Вывод: серверная валидация по составу требований почти полная.
### 3.4 UI и маршруты
В server.js и views/index.ejs уже есть:
1. Главная страница со списком записей.
2. Форма добавления.
3. Soft delete через POST /delete/:id.
4. Экспорт через GET /export.
5. Вывод текущего лимита и числа использованных записей.
6. UI, близкий к стилю Nubes.
Вывод: клиентский сценарий создания и удаления уже работает как MVP.
## 4. Что не соответствует ТЗ или не доведено до конца
### 4.1 Авторизация
Сейчас в server.js не OIDC, а упрощённый decode токена через base64 без проверки подписи. Это подходит только как временная заглушка, но не соответствует ТЗ.
Нужно:
1. Реальная проверка JWT через Keycloak/JWKS или openid-client.
2. Нормальный маппинг claims в user-модель.
3. Явное определение роли admin.
4. Поддержка сценария с несколькими компаниями.
Это главный блокер продакшна.
### 4.2 Редактирование записи
updateEntry уже написана, но:
1. Нет роута POST /update/:id.
2. Нет формы редактирования в index.ejs.
3. Нет пользовательского сценария изменения записи.
То есть требование ТЗ формально не закрыто, хотя код на уровне queries уже есть.
### 4.3 Admin-функции
По ТЗ администратор должен:
1. Видеть записи всех компаний.
2. Фильтровать по компании.
3. Видеть soft-deleted записи.
4. Смотреть аудит.
5. Менять лимиты компаний.
Сейчас ничего из этого не выведено в server.js и views.
### 4.4 Экспорт
GET /export уже есть, но он отдаёт просто список value_cidr без агрегации. ТЗ требует суммаризацию в минимальный набор CIDR по всем активным записям.
Это функциональный разрыв, а не косметика.
### 4.5 Клиентская валидация
ТЗ требует валидацию на клиенте и сервере. Сейчас есть только серверная.
Нужно добавить в форму как минимум:
1. Проверку формата IPv4/CIDR.
2. Проверку диапазона маски.
3. Ограничение длины комментария.
4. Сообщение о возможной нормализации.
### 4.6 Multi-company сценарий
ТЗ явно говорит, что пользователь может принадлежать нескольким компаниям. Сейчас всё построено вокруг одного clientId в req.user.
Нужно:
1. Понять реальный формат claims.
2. Ввести activeCompany в контекст пользователя.
3. Добавить переключатель активной компании в UI.
## 5. Что не надо перепридумывать
1. Не надо переписывать проект на другой язык или другой фреймворк.
2. Не надо менять схему БД ради самой схемы.
3. Не надо переписывать validators.js целиком.
4. Не надо переписывать queries.js целиком.
5. Не надо делать SPA.
Правильный путь: минимально нарастить уже существующую Node.js/Express/EJS реализацию.
## 6. Риски и спорные места
### 6.1 overlaps в validators.js
Условие в overlaps написано нестандартно и плохо читается. Прямого доказанного бага по одной только формуле сейчас нет, но это место требует отдельного теста на:
1. полное совпадение,
2. вложенность,
3. непересекающиеся диапазоны,
4. соседние диапазоны,
5. одиночный IP против подсети.
Решение: сначала добавить точечные тесты, и только потом менять формулу, если тест покажет дефект.
### 6.2 Admin-роль
ТЗ говорит: admin это clientId = WZ01112 и отдельный чек-бокс. Сейчас неясно, как этот чек-бокс попадает в токен. Без этого нельзя финально закрыть auth-модель.
### 6.3 Multi-company claims
ТЗ требует сценарий нескольких компаний, но в списке claims указан только clientID. Здесь нужна конкретика от команды Keycloak.
## 7. Новый план реализации
### Этап 1. Довести до полноты пользовательский сценарий
Цель: закрыть основной client-flow без смены архитектуры.
1. Добавить роут POST /update/:id в server.js.
2. Добавить UI редактирования в views/index.ejs.
3. Показать updated_at и updated_by, если запись менялась.
4. Добавить клиентскую валидацию формы добавления и редактирования.
5. Добавить точечные тесты на overlaps и нормализацию.
Результат этапа: client сможет не только добавлять и удалять, но и редактировать записи, как требует ТЗ.
### Этап 2. Закрыть admin-функциональность
Цель: реализовать недостающую управленческую часть ТЗ.
1. Ввести определение роли admin в auth-слое.
2. Добавить GET /admin.
3. Добавить фильтр по компании.
4. Добавить показ удалённых записей.
5. Добавить GET /admin/audit.
6. Добавить POST /admin/limit/:companyId.
7. Добавить отдельный admin view.
Результат этапа: появляется реальная административная панель, а не только клиентский экран.
### Этап 3. Привести авторизацию к ТЗ
Цель: убрать временную заглушку и сделать реальную интеграцию с Keycloak.
1. Заменить наивный decode токена на верификацию подписи.
2. Добавить конфигурацию issuer, audience, jwks/oidc.
3. Нормализовать claims в req.user.
4. Поддержать admin-claim.
5. Поддержать multi-company claims.
6. Оставить DEV_MODE только для локальной разработки.
Результат этапа: сервис можно выводить из чисто dev-сценария.
### Этап 4. Довести экспорт до требований ТЗ
Цель: сделать экспорт пригодным для систем фильтрации.
1. Добавить суммаризацию активных записей в минимальный набор CIDR.
2. Суммаризировать по всем компаниям совместно.
3. Исключать soft-deleted записи.
4. Оставить выдачу text/plain, одна строка на объект.
5. Отдельно решить, где ограничивается доступ к export endpoint: ingress, app или оба уровня.
Результат этапа: экспорт соответствует ТЗ, а не является просто дампом таблицы.
### Этап 5. Завершение и проверка полноты
1. Сверить все пункты ТЗ с реализованным поведением.
2. Обновить тестовый сценарий.
3. Проверить UX ошибок и предупреждений.
4. Проверить поведение при снижении custom_limit ниже текущего числа активных записей.
5. Проверить сценарии admin/client отдельно.
## 8. Практический приоритет
Если делать не всё сразу, а по реальной важности, порядок такой:
1. Редактирование записи из UI.
2. Клиентская валидация.
3. Admin-панель и лимиты.
4. Реальный OIDC.
5. Multi-company.
6. Суммаризация export.
Почему именно так:
1. Редактирование уже почти готово и закрывает явный пробел ТЗ.
2. Admin-функции сейчас отсутствуют полностью.
3. OIDC блокирует продакшн, но не мешает локально добить функциональность.
4. Экспорт уже работает как черновой endpoint, но должен быть доведён до суммаризации до релиза.
## 9. Итог
Правильный план для этого проекта не “переписать всё правильно”, а “довести уже написанное до требований ТЗ”.
Текущее состояние проекта:
1. Data-layer в основном готов.
2. Server-layer частично готов.
3. UI-layer закрывает только часть client-сценария.
4. Auth-layer пока временный.
5. Admin-layer почти отсутствует.
6. Export-layer не завершён по требованиям агрегации.
Главный вывод: проект ближе к рабочему состоянию, чем казалось по старым планам, но прошлый план был методологически неверен, потому что не опирался на ТЗ и не различал “не написано” и “написано, но не подключено”.
Эта часть критична. Её нужно делать одной из первых и сразу покрывать тестами.
### Поддерживаемый ввод
1. Одиночный IPv4 адрес, который трактуется как /32.
2. IPv4 подсеть в CIDR нотации.
### Запрещённый ввод
1. IPv6.
2. Доменное имя.
3. Маска шире допустимой.
4. Любые private или special ranges из приложения А.
### Правила маски
Допустимы только /22 ... /32.
### Нормализация
Если пользователь ввёл адрес с host-битами, сервис должен:
1. Нормализовать значение до адреса сети.
2. Сохранить нормализованное значение.
3. Вернуть пользователю явное сообщение, что адрес был нормализован.
### Проверки в пределах компании
1. Запрет полного дубликата активной записи.
2. Запрет любого пересечения активной записи с существующими активными записями той же компании.
3. Между разными компаниями пересечения допускаются.
### Список запрещённых диапазонов
Нужно захардкодить как конфигурацию приложения и покрыть тестами:
1. 10.0.0.0/8
2. 172.16.0.0/12
3. 192.168.0.0/16
4. 100.64.0.0/10
5. 127.0.0.0/8
6. 169.254.0.0/16
7. 192.0.0.0/24
8. 192.0.2.0/24
9. 198.51.100.0/24
10. 203.0.113.0/24
11. 198.18.0.0/15
12. 224.0.0.0/4
13. 240.0.0.0/4
14. 255.255.255.255/32
## 10. Лимиты и конкурентность
Это важное место, которого обычно недооценивают.
### Правила лимитов
1. Есть глобальный DEFAULT_LIMIT, по умолчанию 15.
2. Для компании может быть custom_limit.
3. При снижении лимита ниже текущего количества записей существующие записи не удаляются.
4. Пока число активных записей больше лимита, новые записи создавать нельзя.
### Риск гонок
Если два запроса одновременно создают записи в одной компании, возможны:
1. Пробитие лимита.
2. Пропуск пересечения.
3. Пропуск дубликата.
### Что делать
Операцию создания и обновления записи нужно делать в транзакции с сериализацией логики на уровне компании. Практически это можно решить так:
1. Брать advisory lock по company_id перед проверками и записью.
2. Либо делать SELECT ... FOR UPDATE по строке компании, если этого достаточно для вашей схемы доступа.
Для первой версии я бы выбрал advisory lock по company_id. Это проще и надёжнее для бизнес-ограничений, которые нельзя полностью выразить обычным unique index.
## 11. Экспорт агрегированного списка
### Требования
1. В экспорт попадают только активные записи.
2. Данные берутся по всем компаниям.
3. Пересечения между компаниями допустимы на уровне хранения, но в export должны агрегироваться в минимальный набор CIDR.
4. Формат ответа: text/plain.
5. Одна строка = один CIDR.
### Что нужно зафиксировать реализационно
1. Результат должен быть отсортирован для стабильности.
2. В ответе должен быть завершающий перевод строки.
3. Content-Type должен быть text/plain; charset=utf-8.
4. Желательно отдавать Content-Disposition с понятным именем файла.
### Защита endpoint
Если endpoint на старте работает без auth, то доступ надо ограничить минимум одним из способов:
1. Проверка client IP в приложении.
2. Ограничение на reverse proxy.
3. Оба сразу.
## 12. UI-потоки
### Экран клиента
Должны быть:
1. Селектор активной компании, если компаний несколько.
2. Таблица записей.
3. Индикатор использовано X из N.
4. Форма создания записи.
5. Возможность редактирования.
6. Возможность soft delete.
### Экран администратора
Должны быть:
1. Таблица по всем компаниям.
2. Фильтр по компании.
3. Фильтр показа удалённых записей.
4. Просмотр журнала аудита.
5. Управление лимитами компании.
### UX-детали, которые обязательно сделать
1. Понятные сообщения об ошибках валидации.
2. Явное сообщение о нормализации адреса.
3. Явное сообщение о превышении лимита.
4. Явное сообщение о пересечении с существующей записью.
## 13. Пошаговый план реализации
### Этап 1. Каркас проекта
1. Создать структуру каталогов.
2. Подготовить requirements.txt.
3. Подготовить .env.example.
4. Подключить FastAPI, Jinja2, static.
5. Подготовить docker-compose.yml с PostgreSQL.
Результат этапа: приложение стартует, открывается базовая страница, есть подключение к БД.
### Этап 2. Схема БД и миграции
1. Настроить Alembic.
2. Создать initial migration.
3. Поднять таблицы companies, whitelist_entries, audit_log.
4. Добавить нужные индексы.
Результат этапа: схема БД фиксирована и воспроизводима.
### Этап 3. Валидатор CIDR
1. Реализовать разбор IPv4 и CIDR.
2. Реализовать проверку маски.
3. Реализовать нормализацию.
4. Реализовать проверку запрещённых диапазонов.
5. Написать тесты на валидатор.
Результат этапа: независимый, протестированный модуль бизнес-валидации.
### Этап 4. Сервисный слой для записей
1. Реализовать list.
2. Реализовать create.
3. Реализовать update.
4. Реализовать soft delete.
5. Реализовать проверки лимитов, дубликатов и пересечений.
6. Добавить транзакционную защиту от гонок.
Результат этапа: бизнес-операции работают без UI.
### Этап 5. Аудит
1. Добавить запись CREATE.
2. Добавить запись UPDATE со старым и новым состоянием.
3. Добавить запись DELETE.
4. Добавить интерфейс чтения для admin.
Результат этапа: все изменяющие действия фиксируются.
### Этап 6. Авторизация
1. Реализовать dev-заглушку.
2. Реализовать чтение и валидацию JWT из Keycloak.
3. Реализовать преобразование claims в current user.
4. Реализовать проверки client/admin.
5. Реализовать переключение компании.
Результат этапа: права и контекст пользователя работают сквозным образом.
### Этап 7. HTML-интерфейс
1. Реализовать страницу списка.
2. Реализовать формы создания и редактирования.
3. Реализовать soft delete из UI.
4. Реализовать админские экраны.
Результат этапа: сервис пригоден для ручной эксплуатации.
### Этап 8. Экспорт
1. Реализовать сбор всех активных CIDR.
2. Реализовать агрегацию.
3. Реализовать endpoint export.
4. Реализовать сетевое ограничение.
Результат этапа: внешняя система может забирать текстовый агрегированный whitelist.
### Этап 9. Финализация
1. Написать README.
2. Подготовить Dockerfile.
3. Подготовить пример systemd unit при необходимости.
4. Прогнать ручной smoke-test.
Результат этапа: сервис можно разворачивать и передавать коллегам.
## 14. Тестовая стратегия
Минимально обязательные тесты:
1. Валидный одиночный IPv4 превращается в /32.
2. Валидная подсеть принимается.
3. Host-биты нормализуются.
4. Маски шире допустимой границы отклоняются.
5. IPv6 отклоняется.
6. Все запрещённые диапазоны отклоняются.
7. Дубликат в одной компании запрещён.
8. Пересечение в одной компании запрещено.
9. Тот же CIDR в другой компании разрешён.
10. Soft delete освобождает лимит.
11. Export не включает soft-deleted записи.
12. Export агрегирует CIDR корректно.
13. Client не видит чужие компании.
14. Admin видит все компании.
15. Аудит создаётся для create, update, delete.
## 15. Что можно отложить после первой версии
Это не нужно тащить в MVP:
1. Полноценный SPA.
2. Сложная ORM.
3. WebSocket.
4. Фоновая очередь.
5. Исторические версии записей кроме audit log.
6. Автоматическое уведомление по email.
## 16. Главные риски проекта
1. Неясный формат claims из Keycloak.
2. Гонки при одновременном создании записей.
3. Ошибки в трактовке пересечений CIDR.
4. Неправильная нормализация адресов без понятного сообщения пользователю.
5. Слишком раннее усложнение фронтенда.
## 17. Что я бы делал первым
Если начинать реализацию прямо сейчас, порядок такой:
1. Каркас проекта.
2. Схема БД.
3. Валидатор и тесты.
4. Сервис create/update/delete с транзакциями.
5. Только потом UI и Keycloak.
Это самый безопасный путь: сначала фиксируется ядро бизнес-логики, потом уже внешний слой.
## 18. Итоговое решение
За основу реализации стоит брать простой Python/FastAPI сервис с PostgreSQL, синхронной серверной логикой, жёсткой серверной валидацией, транзакционной защитой от гонок и минималистичным HTML UI.
Главная мысль: сложность здесь не во фронтенде и не в фреймворке, а в корректной реализации правил CIDR, лимитов, ролей и аудита. План должен защищать именно эти части, а не раздувать стек.
-188
View File
@@ -1,188 +0,0 @@
# План разработки — IP WhiteList Microservice v2
> **Автор:** GitHub Copilot (Claude Sonnet 4.6)
> **Дата:** 2026-05-29
---
## Стек
| Слой | Технология | Обоснование |
|---|---|---|
| Бэкенд | Python 3.11+ / FastAPI | Коллеги знают Python, авто-документация |
| БД | PostgreSQL | Надёжно, поддерживает аудит и сложные запросы |
| Работа с БД | psycopg2 + сырой SQL | Проще чем ORM, понятно всем, никакой магии |
| Миграции | Alembic | Только для версионирования схемы |
| Фронтенд | Jinja2 + обычные HTML-формы | Без JS-фреймворков, минимум зависимостей |
| Авторизация | Keycloak OIDC (JWT) | Заглушка только в dev через `.env` флаг |
| IP-логика | stdlib `ipaddress` + `netaddr` | Суммаризация CIDR через `netaddr` |
---
## Открытые вопросы (нужно прояснить до кодирования)
1. **Чекбокс администратора** — ТЗ: admin = `clientId == WZ01112` + «отдельный чек-бокс». Что это: отдельный claim в Keycloak-токене (`is_admin: true`)? Роль? Нужно уточнить у команды Keycloak.
2. **Создание Company в БД** — когда появляется запись: при первом входе пользователя автоматически, или администратор заводит вручную?
3. **Кто потребляет внешний endpoint** — endpoint без авторизации, доступ по IP. Список доверенных IP задаётся конфигом? Nginx ACL?
---
## Файловая структура
```
IPWhiteList/
├── app/
│ ├── main.py # FastAPI app, роутеры, startup
│ ├── config.py # Настройки из .env (DEFAULT_LIMIT, DEV_MODE, DB_DSN и др.)
│ ├── db.py # psycopg2 connection pool
│ ├── models/
│ │ └── sql.py # DDL-схема (только для документации, не ORM)
│ ├── validators.py # IPv4/CIDR: формат, маска, серые адреса, нормализация
│ ├── crud/
│ │ ├── entries.py # CRUD whitelist_entries
│ │ ├── companies.py # Компании и лимиты
│ │ └── audit.py # Запись в audit_log
│ ├── auth/
│ │ ├── oidc.py # Валидация JWT Keycloak
│ │ ├── stub.py # Dev-заглушка (только при DEV_MODE=true)
│ │ └── deps.py # FastAPI Depends: current_user
│ ├── routers/
│ │ ├── entries.py # CRUD UI-роуты + HTMX-фрагменты
│ │ ├── admin.py # Аудит, лимиты (только admin)
│ │ └── external.py # GET /api/v1/export — txt-файл
│ └── cidr_utils.py # Суммаризация через netaddr
├── templates/
│ ├── base.html
│ ├── index.html # Таблица записей + индикатор лимита
│ ├── partials/
│ │ ├── table.html # HTMX-фрагмент таблицы
│ │ └── form.html # Форма создания/редактирования
│ └── admin/
│ ├── audit.html
│ └── limits.html
├── static/
│ └── style.css
├── migrations/
│ ├── env.py
│ └── versions/
├── tests/
│ ├── test_validators.py # Юниты для IPv4-валидации (критично!)
│ └── test_crud.py
├── docs/
│ ├── plan.md # LEGACY
│ ├── plan-v2.md # Этот файл
│ └── WhiteIPlist.docx # Исходное ТЗ
├── .env.example
├── alembic.ini
├── docker-compose.yml # PostgreSQL для dev
├── requirements.txt
└── README.md
```
---
## Схема БД
```sql
-- Компании (создаются автоматически при первом входе или вручную админом — уточнить)
CREATE TABLE companies (
id SERIAL PRIMARY KEY,
client_id VARCHAR(64) UNIQUE NOT NULL, -- из Keycloak claim
name VARCHAR(255),
custom_limit INTEGER DEFAULT NULL -- NULL = использовать глобальный DEFAULT_LIMIT
);
-- Whitelist-записи
CREATE TABLE whitelist_entries (
id SERIAL PRIMARY KEY,
company_id INTEGER NOT NULL REFERENCES companies(id),
value CIDR NOT NULL, -- нормализованный CIDR
comment VARCHAR(255),
created_by VARCHAR(255) NOT NULL, -- email из токена
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by VARCHAR(255),
updated_at TIMESTAMPTZ,
deleted_by VARCHAR(255),
deleted_at TIMESTAMPTZ -- NULL = активная запись
);
-- Аудит (только append, без UPDATE/DELETE)
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
user_email VARCHAR(255) NOT NULL,
company_id INTEGER NOT NULL,
action VARCHAR(32) NOT NULL, -- CREATE | UPDATE | DELETE
old_value TEXT,
new_value TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Индексы
CREATE INDEX ON whitelist_entries(company_id) WHERE deleted_at IS NULL;
CREATE INDEX ON audit_log(company_id);
```
---
## Этапы
### Этап 1 — Каркас + конфиг
- [ ] Структура папок
- [ ] `requirements.txt`: fastapi, uvicorn, psycopg2-binary, alembic, jinja2, python-jose, netaddr
- [ ] `.env.example` со всеми переменными: `DB_DSN`, `DEFAULT_LIMIT=15`, `DEV_MODE=false`, `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `ALLOWED_EXPORT_IPS`
- [ ] `config.py` — читает `.env`, все параметры типизированы
- [ ] `db.py` — psycopg2 connection pool (SimpleConnectionPool)
- [ ] `docker-compose.yml` с PostgreSQL
### Этап 2 — Миграции (схема БД)
- [ ] Alembic init
- [ ] Initial migration: `companies`, `whitelist_entries`, `audit_log` + индексы
- [ ] Проверка `alembic upgrade head`
### Этап 3 — Валидатор IPv4 (с тестами)
- [ ] `validators.py`: принимает строку → возвращает нормализованный CIDR или ошибку
- [ ] Проверка формата: одиночный IP или CIDR
- [ ] Проверка маски: /22 – /32 (шире /21 — `ValidationError`)
- [ ] Нормализация host-битов: `192.168.1.5/24``192.168.1.0/24` + флаг `was_normalized=True`
- [ ] Запрет серых диапазонов (все из Приложения А ТЗ)
- [ ] `tests/test_validators.py` — покрыть все граничные случаи
### Этап 4 — CRUD-логика
- [ ] `crud/companies.py`: get_or_create по client_id, get_limit (custom_limit ?? DEFAULT_LIMIT)
- [ ] `crud/entries.py`: список активных, создание (лимит + дубликаты + пересечения), редактирование, soft-delete
- [ ] `crud/audit.py`: append-only запись
### Этап 5 — Авторизация
- [ ] `auth/oidc.py` — валидация JWT через JWKS Keycloak, извлечение `clientID`, `email`, определение роли
- [ ] Логика роли admin: `clientID == WZ01112` + (claim `is_admin == true`**уточнить**)
- [ ] `auth/stub.py` — только при `DEV_MODE=true`: читает `X-Dev-User` из заголовка
- [ ] `auth/deps.py``Depends(current_user)` для роутеров
### Этап 6 — Роутеры + UI
- [ ] `routers/entries.py`: список, форма создания, форма редактирования, удаление, переключатель компании
- [ ] `routers/admin.py`: журнал аудита, управление лимитами
- [ ] Шаблоны Jinja2: base.html, index.html, form.html, admin/audit.html, admin/limits.html
- [ ] Индикатор лимита «X из N» на странице
- [ ] Уведомление о нормализации адреса пользователю
### Этап 7 — Внешний endpoint
- [ ] `GET /api/v1/export` — только активные записи всех компаний
- [ ] Суммаризация через `netaddr.cidr_merge()`
- [ ] Ответ: `text/plain`, одна строка — один CIDR
- [ ] IP-фильтр из `ALLOWED_EXPORT_IPS` (middleware или Depends)
### Этап 8 — Деплой
- [ ] `Dockerfile` (python:3.11-slim, uvicorn)
- [ ] Systemd unit как альтернатива
- [ ] Nginx конфиг: reverse proxy + location для static
- [ ] README: как поднять с нуля
---
## Ключевые принципы
- **Синхронный код везде** — никакого async/await. FastAPI поддерживает синхронные роутеры.
- **Серверная валидация — авторитетная**. Клиентская — только UX.
- **`DEV_MODE=true`** — единственный способ обойти Keycloak. В prod недоступен.
- **Audit log — append only**. Никаких UPDATE/DELETE в `audit_log`.
- **Лимит `DEFAULT_LIMIT`** — всегда из `config.py`, который читает `.env`. Без пересборки.
-103
View File
@@ -1,103 +0,0 @@
# Рабочий план — IP WhiteList
> **Автор:** GitHub Copilot (Claude Sonnet 4.6)
> **Дата:** 2026-05-29
> **Стек:** Node.js + Express + pg + EJS
> **Деплой:** nubes_nodejs + nubes_postgres (через веб-кабинет)
---
## Что выяснили
| Факт | Детали |
|---|---|
| **API шлюз** | `lk-api-gateway.ngcloud.ru`, за DDOS-Guard |
| **JWT** | `iss: auth-api`, claims: `ClientID`, `company_id`, `company_name`, `email` |
| **Токен для dev** | `secrets.txt`, tech-токен, долгий |
| **Валидация JWT** | Шлюз делает сам, нам не нужно |
| **Деплой** | Пользователь создаёт инстансы через веб-кабинет |
| **isAdmin** | ❓ В JWT нет, нужно уточнить как передавать |
| **Мульти-компании** | ❓ В JWT одна компания, список — в authData (localStorage) |
---
## Файлы проекта (всё в корне)
```
├── server.js # Express: старт, роуты, middleware
├── db.js # pg Pool
├── .env.example # DB_DSN, PORT, DEFAULT_LIMIT, DEV_MODE
├── package.json
├── views/ # EJS-шаблоны
│ └── index.ejs # таблица + форма
├── public/
│ └── style.css
├── sql/
│ └── schema.sql # CREATE TABLE companies, whitelist_entries, audit_log
└── .gitignore
```
---
## Схема БД
```sql
companies (id, client_id UNIQUE, name, custom_limit)
whitelist_entries (id, company_id FK, value_cidr, comment, created_by, created_at, updated_by, updated_at, deleted_by, deleted_at)
audit_log (id, user_email, company_id, action, old_value, new_value, created_at)
```
- `deleted_at IS NULL` = активная запись
- `custom_limit IS NULL` = использовать DEFAULT_LIMIT (15)
---
## Порядок действий
### Шаг 1 — Каркас
- package.json (express, pg, ejs, dotenv)
- server.js (Express, EJS, static, health check `/healthz`)
- db.js (pg Pool, `SELECT 1` при старте)
- .env.example
### Шаг 2 — Схема БД
- sql/schema.sql
- Запустить на своём PG
### Шаг 3 — Валидатор IPv4
- Функция validateCIDR(input) → { cidr, wasNormalized } | error
- Правила: /32–/22, запрет серых, нормализация host-битов
### Шаг 4 — CRUD (сырой pg, без ORM)
- Список записей компании
- Создание (проверка лимита, дубликатов, пересечений)
- Редактирование
- Soft-delete
### Шаг 5 — Auth middleware
- DEV_MODE=true: читать заголовок X-Dev-User
- PROD: читать JWT из Authorization (шлюз уже проверил)
### Шаг 6 — UI
- Таблица + форма создания/редактирования
- Индикатор лимита «X из N»
- Сообщения: нормализация, превышение, пересечение
### Шаг 7 — Аудит
- Запись в audit_log при create/update/delete
### Шаг 8 — Экспорт
- GET /api/v1/export — txt, все активные CIDR
### Шаг 9 — Деплой
- Завести nubes_postgres и nubes_nodejs через кабинет
- Подключить к API-шлюзу (уточнить процедуру)
---
## Что откладываем
- Keycloak OIDC (шлюз делает)
- Полноценный админ-интерфейс
- Переключатель компаний (multi-profile)
- Тонкая настройка прав
-107
View File
@@ -1,107 +0,0 @@
# IP WhiteList — План разработки
> **Дата:** 2026-05-30
> **Основание:** [WhiteIPlist.txt](../WhiteIPlist.txt) (ТЗ) + фактический код в [ipwhitelist-app](../../ipwhitelist-app)
> **Статус:** каноничный — единственный актуальный план
---
## 1. Текущее состояние
### Готово ✅
| Слой | Файлы | Что есть |
|---|---|---|
| БД | `sql/schema.sql` | `companies`, `whitelist_entries` (soft delete), `audit_log` + индексы |
| Валидатор | `src/validators.js` | Все 14 запрещённых диапазонов Приложения А, /22–/32, нормализация host-битов, проверка пересечений |
| CRUD | `src/queries.js` | `createEntry`, `updateEntry`, `deleteEntry` (soft), `listEntries`, `getExportCIDRs`, `getAudit`, `getLimit` |
| UI | `views/index.ejs` | Список, форма добавления, кнопка удаления, стиль Nubes |
| Роуты | `server.js` | `GET /`, `POST /add`, `POST /delete/:id`, `GET /export`, `GET /healthz` |
### Написано в queries.js, но не подключено к роутам
- `updateEntry` — нет `POST /update/:id`, нет UI редактирования
- `getAudit` — нет страницы аудита
- `listEntries(companyId, includeDeleted=true)` — флаг есть, не используется
---
## 2. Что требует ТЗ, но отсутствует
| # | Требование | Готовность |
|---|---|---|
| 1 | Редактирование записи из UI | ❌ queries есть, роута нет |
| 2 | Админ-панель (все компании, лимиты, аудит, фильтры) | ❌ |
| 3 | OIDC Keycloak вместо base64-заглушки | ❌ |
| 4 | Суммаризация CIDR в `/export` | ❌ отдаёт сырой список |
| 5 | Клиентская валидация (JS в форме) | ❌ |
| 6 | Переключатель компаний (multi-company) | ❌ |
| 7 | Просмотр soft-deleted записей админом | ❌ |
| 8 | Изменение `custom_limit` для компании | ❌ |
---
## 3. Порядок реализации
### Этап 1 — Пользовательский сценарий (client-flow)
- [x] Валидатор IPv4/CIDR — полный
- [x] Создание / удаление / экспорт
- [ ] **Добавить `POST /update/:id`** в `server.js`
- [ ] **Добавить inline-форму редактирования** в `views/index.ejs`
- [ ] **Клиентская валидация** (JS: формат, маска, длина комментария)
### Этап 2 — Административная панель
- [ ] Определение admin-роли (`clientId === 'WZ01112'` + чекбокс)
- [ ] `GET /admin` — страница со всеми компаниями
- [ ] `POST /admin/limit/:companyId` — изменение custom_limit
- [ ] `GET /admin/audit` — журнал аудита
- [ ] Фильтр по компании + показ удалённых записей
### Этап 3 — Авторизация Keycloak OIDC
- [ ] `npm install openid-client`
- [ ] Замена base64-decode на проверку подписи JWT
- [ ] Маппинг claims → `req.user` (clientId, email, role)
- [ ] Оставить `DEV_MODE` только для локальной разработки
### Этап 4 — Экспорт и Multi-company
- [ ] `npm install cidr-tools` — суммаризация в `GET /export`
- [ ] Переключатель активной компании (если несколько `clientId` в claims)
### Этап 5 — Завершение
- [ ] Автотесты (`jest` + `supertest`)
- [ ] Пагинация (если лимит > 50)
- [ ] Сверка всех пунктов ТЗ
---
## 4. Что НЕ делать
- ❌ Не переписывать на Python/FastAPI
- ❌ Не менять схему БД
- ❌ Не переписывать `validators.js` (он полный)
- ❌ Не переписывать `queries.js`
- ❌ Не делать SPA
---
## 5. Зависимости для установки
```
npm install cidr-tools openid-client
npm install --save-dev jest supertest
```
Всё остальное — в рамках Express + EJS + pg.
---
## 6. Риски
- **Admin-роль:** неясно как «отдельный чек-бокс» из ТЗ попадает в токен — требует уточнения с командой Keycloak
- **Multi-company claims:** ТЗ говорит о нескольких компаниях, но в claims только `clientID` — нужен реальный формат
- **IP-ограничение `/export`:** делать в приложении или на уровне ingress — решить при деплое
-169
View File
@@ -1,169 +0,0 @@
# IP WhiteList — Архитектура (актуально, 2026-05-30 14:43)
> Ветка `sonnet`, репо: `gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git`
> Код: `/home/naeel/ipwhitelist-app`
---
## Стек
| Слой | Технология |
|---|---|
| Сервер | Node.js + Express 4 |
| Шаблоны | EJS (server-side, без SPA) |
| БД | PostgreSQL, клиент `pg` (pool) |
| Безопасность | helmet, express-rate-limit, csrf-csrf, express-session |
| JWT | jsonwebtoken (mock RS256 / OIDC validation) |
| Авторизация | Keycloak (OIDC) или mock при локальной разработке |
---
## Структура модулей
```
server.js — точка входа: init + подключение роутеров (131 строк)
src/
auth.js — аутентификация: OIDC или mock, session middleware
config.js — MOCK_USERS, backUrl()
db.js — pg pool (checkConnection)
queries.js — все SQL-запросы к БД
validators.js — validateCIDR, aggregateCIDRs
middleware/
csrf.js — initCsrf() → { doubleCsrfProtection, generateCsrfToken }
rateLimit.js — mutationLimiter (30/мин), exportLimiter (20/мин)
routes/
auth.js — /login /callback /logout /dev-login
entries.js — / /add /edit/:id /delete/:id
admin.js — /audit /admin /admin/limit/:companyId
export.js — /export
views/
login.ejs — форма входа (mock) или заглушка (OIDC)
dev-login.ejs — тестовый вход (пресеты + произвольные поля)
index.ejs — список записей пользователя / admin-обзор
admin.ejs — admin-панель (все компании, лимиты)
audit.ejs — лог операций
sql/
schema.sql — companies, whitelist_entries (soft delete), audit_log
```
---
## Схема авторизации
```
GET /login
┌─ OIDC-режим (KC_CLIENT_ID задан) ──────────────────────────────────────┐
│ state → сессия │
│ redirect → keycloak.nubes.ru/realms/cloud/.../auth │
│ ↓ KK перенаправляет на /callback │
│ GET /callback → проверить state → exchangeCode → токен KK │
│ → userFromPayload → req.session.user → redirect / │
└─────────────────────────────────────────────────────────────────────────┘
┌─ Mock-режим (KC_CLIENT_ID не задан) ───────────────────────────────────┐
│ render login.ejs (выбрать из MOCK_USERS) │
│ POST /login → CSRF → req.session.user → redirect / │
└─────────────────────────────────────────────────────────────────────────┘
GET /dev-login (DEV_MODE=true или DEV_SECRET задан)
render dev-login.ejs (пресеты MOCK_USERS + произвольные поля)
POST /dev-login → CSRF → req.session.user → redirect /
⚠ Работает в ОБОИХ режимах — для тестирования с разными ролями
GET /logout
session.destroy() + clearCookie('connect.sid')
OIDC: redirect → keycloak logout endpoint
Mock: redirect → /login
Все защищённые роуты: auth.middleware
req.session.user → req.user (основной путь)
Authorization: Bearer → verify JWT → req.user (API-клиенты)
cookie 'jwt' (legacy) → verify → session → req.user (плавная миграция)
нет ничего → GET: redirect /login, остальное: 401
```
---
## JWT claims (auth-api, из реального токена портала)
> auth-api выпускает свой JWT — **не Keycloak напрямую**.
> Issuer: `"auth-api"`, алгоритм: RS256.
> Наш сервис принимает и валидирует этот токен.
| Поле | Тип | Пример | Что используем |
|---|---|---|---|
| `ClientID` | string | `"WZ01325"` | `req.user.clientId` |
| `company_id` | UUID | `"3e64aac6-..."` | `req.user.companyId`, FK в БД |
| `company_name` | string | `"Тест"` | `req.user.companyName` |
| `email` | string | `"user@example.com"` | `req.user.email` (аудит) |
| `login` | string | `"user@example.com"` | fallback для email |
| `sub` | UUID | `"0199e325-..."` | fallback для companyId |
| `iss` | string | `"auth-api"` | проверяется при валидации |
| `exp` | number | ~12 часов | автоматически |
**Признак admin:** `clientId === ADMIN_CLIENT_ID` (env `ADMIN_CLIENT_ID`, default `WZ01112`).
`isAdmin` не приходит в JWT — см. [questions.md](questions.md).
---
## Маршруты
| Метод | Путь | Доступ | Лимит |
|---|---|---|---|
| GET | `/healthz` | public | — |
| GET | `/.well-known/jwks.json` | public | — |
| GET | `/export` | public | 20/мин |
| GET/POST | `/login` | public | — |
| GET | `/callback` | public | — |
| GET | `/logout` | public | — |
| GET/POST | `/dev-login` | public (с guard) | — |
| GET | `/` | auth | — |
| POST | `/add` `/edit/:id` `/delete/:id` | auth | 30/мин |
| GET | `/audit` `/admin` | auth + admin | — |
| POST | `/admin/limit/:companyId` | auth + admin | 30/мин |
---
## Env-переменные
### Обязательные в продакшене
| Переменная | Описание |
|---|---|
| `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASS` | PostgreSQL |
| `SESSION_SECRET` | Секрет сессии (≥ 32 символа) |
| `CSRF_SECRET` | Секрет CSRF (≥ 32 символа) |
| `KC_CLIENT_ID` | client_id в Keycloak realm cloud |
| `KC_CLIENT_SECRET` | client_secret |
| `APP_URL` | Внешний URL приложения (для redirect_uri) |
### Опциональные
| Переменная | Default | Описание |
|---|---|---|
| `KC_BASE_URL` | `https://keycloak.nubes.ru/realms/cloud` | Keycloak realm base URL |
| `ADMIN_CLIENT_ID` | `WZ01112` | WZ-номер администратора |
| `PORT` | `3000` | HTTP-порт |
| `NODE_ENV` | — | `production` включает secure cookie, trust proxy |
| `DEV_MODE` | `false` | `true` → /dev-login без пароля |
| `DEV_SECRET` | — | Ключ для /dev-login в staging |
---
## БД (dev)
```
Host: write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru
DB: ipwhitelist
User: super
```
Схема: `sql/schema.sql``companies`, `whitelist_entries` (soft delete), `audit_log` + индексы.
---
## Деплой (текущий)
- URL: `https://white.nodejsk8s.dev.nubes.ru`
- k8s namespace: `whitelist`
- Сессии: in-memory (MemoryStore) — **не масштабируется**, нужен Redis при multi-pod
- Режим: ожидает KC_CLIENT_ID/KC_CLIENT_SECRET для перехода с mock на OIDC
-112
View File
@@ -1,112 +0,0 @@
# Дизайн-система платформы 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` |
-96
View File
@@ -1,96 +0,0 @@
# IP WhiteList — План (pending задачи, 2026-05-30 14:43)
> **Ветка:** `sonnet` · **Обновлено:** 2026-05-30 14:43
> Всё что было в старых планах — реализовано. Здесь только то, чего ещё нет.
---
## Блокер 1 — Keycloak OIDC (нужны данные от DevOps)
Код OIDC Authorization Code Flow написан и готов (`src/auth.js`).
Не активируется пока не получены:
| Что нужно | Env-переменная | Статус |
|---|---|---|
| client_id в realm cloud | `KC_CLIENT_ID` | ❌ ждём DevOps |
| client_secret | `KC_CLIENT_SECRET` | ❌ ждём DevOps |
| redirect_uri зарегистрирован в KK | (в самом KK) | ❌ ждём DevOps |
| SESSION_SECRET для продакшена | `SESSION_SECRET` | ❌ нужно сгенерировать |
Без этого сервис работает в mock-режиме (`/dev-login` как тестовый backdoor).
---
## Блокер 2 — Admin-роль из Keycloak
Сейчас admin = `clientId === WZ01112`.
Как реально приходит `isAdmin` из KK — не ясно (см. [questions.md](questions.md)).
После получения ответа — 1–2 строки в `userFromPayload()` в `src/auth.js`.
---
## Задача 1 — Деплой ветки sonnet
- [ ] Передеплоить на `white.nodejsk8s.dev.nubes.ru` (сейчас там master)
- [ ] Добавить env-секреты в k8s: `SESSION_SECRET`, `CSRF_SECRET`
- [ ] Проверить smoke-тест: `/healthz`, `/login`, `/export`
---
## Задача 2 — Сессии при multi-pod
Сейчас: MemoryStore (in-process, не масштабируется).
При нескольких репликах k8s сессии будут теряться.
- [ ] `npm install connect-redis ioredis`
- [ ] Подключить Redis в `server.js` (3 строки конфига)
- [ ] Добавить `REDIS_URL` в env
**Блокер:** нужен Redis в k8s (или принять ограничение на 1 реплику пока).
---
## Задача 3 — IP-ограничение /export
ТЗ: endpoint доступен без авторизации, ограничен по IP на старте.
Текущее решение: открытый endpoint с rate-limit (20 req/мин).
- [ ] Уточнить: ограничение в приложении или ingress? (см. [questions.md](questions.md))
- [ ] Если в приложении: whitelist IP через `EXPORT_ALLOWED_IPS` env var
---
## Задача 4 — Пагинация
При `custom_limit` > 50 таблица станет неудобной.
Сейчас все записи выводятся без пагинации.
- [ ] `GET /?page=N` + `LIMIT/OFFSET` в `queries.listEntries()`
- [ ] Кнопки Назад/Вперёд в `views/index.ejs`
Низкий приоритет — текущий дефолтный лимит 15 записей на компанию.
---
## Задача 5 — Auto-dismiss alert
Flash-алерты (ошибка/успех) сейчас исчезают только при перезагрузке.
- [ ] 10 строк JS в `views/index.ejs`: `setTimeout(() => alert.remove(), 5000)`
---
## Не делать (закрыто)
| Что | Почему |
|---|---|
| ~~CSRF-защита~~ | ✅ csrf-csrf (double-submit cookie) |
| ~~Рефакторинг server.js~~ | ✅ 131 строка, роутеры вынесены |
| ~~Автотесты~~ | ✅ 50 unit-тестов (`node tests/run-tests.js`) |
| ~~aggregateCIDRs в /export~~ | ✅ в `src/validators.js` |
| ~~Admin-панель~~ | ✅ `src/routes/admin.js`, `views/admin.ejs` |
| ~~Редактирование записи~~ | ✅ `POST /edit/:id` |
| ~~Аудит~~ | ✅ `GET /audit` |
| ~~OIDC Authorization Code Flow~~ | ✅ `src/auth.js` + `src/routes/auth.js` |
| ~~Session middleware~~ | ✅ `express-session` в `server.js` |
| ~~Dev-login backdoor~~ | ✅ `/dev-login` с пресетами + кастомные поля |
-70
View File
@@ -1,70 +0,0 @@
# Вопросы к DevOps / команде Keycloak — IP WhiteList
> Обновлено: 2026-05-30 14:43
> Часть вопросов из первоначального списка закрыта реализацией.
---
## ❌ ОТКРЫТЫЕ (блокируют деплой)
### 1. Данные клиента Keycloak
Для активации OIDC-режима (код готов, ждёт переменных):
```
KC_CLIENT_ID= ? # наш client_id в realm cloud
KC_CLIENT_SECRET= ? # наш client_secret
```
Redirect URI для регистрации в Keycloak:
```
https://white.nodejsk8s.dev.nubes.ru/callback
```
---
### 2. Admin-роль: как приходит из Keycloak
**ТЗ:** Администратор = `clientId = WZ01112` + «отдельный чек-бокс».
Сейчас: `isAdmin = (ClientID === process.env.ADMIN_CLIENT_ID)` — только WZ01112.
Вопросы:
- Есть ли отдельный claim в JWT для признака admin?
- Если да — как называется? Примеры: `realm_access.roles`, `resource_access.whitelist.roles`, `is_admin`, `groups`
- Нужна поддержка нескольких adminов или только WZ01112?
**Если нет отдельного claim** — оставляем текущее решение, закрываем вопрос.
---
### 3. /export — IP-ограничение
ТЗ: «на старте может работать без авторизации (по сетевому ограничению)».
Сейчас: открытый endpoint, rate-limit 20 req/мин.
- Ограничение делаем в **приложении** или в **ingress/nginx**?
- Если в приложении — список разрешённых IP (env var `EXPORT_ALLOWED_IPS`?).
---
## ✅ ЗАКРЫТЫЕ
| # | Вопрос | Решение |
|---|---|---|
| DEV_MODE | Нужен ли dev-режим на платформе? | `/dev-login` — backdoor с `DEV_MODE=true` (без пароля) или `DEV_SECRET=xyz` (с ключом). Работает в обоих режимах. |
| Email claim | Как называется claim с email? | Берём `payload.email \|\| payload.login \|\| 'unknown'` |
| JWT структура | Формат токена auth-api | Изучен из реального токена (см. [DEPRECATED]-auth-architecture.md). Поля: `ClientID`, `company_id`, `company_name`, `email`, `login`. |
| CSRF | Защита POST-форм | csrf-csrf (double-submit cookie pattern) |
| Session | Хранение пользователя | express-session (httpOnly, sameSite lax, 8ч) |
---
## Условные обозначения в коде
```js
// TODO(KK): уточнить формат claim — см. docs/questions.md#2
// FIXME(KK): временно, заменить после ответа команды
```
-320
View File
File diff suppressed because one or more lines are too long
-31257
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
# research/
Материалы обратного инжиниринга платформы Nubes — анализ трафика, HAR, токены, схемы взаимодействия.
В отличие от `docs/` (проектная документация), здесь — результаты исследования внешних систем, которые мы не контролируем.
-134
View File
@@ -1,134 +0,0 @@
# Сводка код-ревью IP WhiteList (все 5 файлов)
> Дата: 2026-05-30
## 🔴 Критические (блокеры прода)
### 1. JWT без проверки подписи — server.js
```js
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
```
Декодирует payload без верификации подписи. Любой подделывает токен → любая компания. Нужна `jose.jwtVerify(token, JWKS, { issuer: 'auth-api' })`.
### 2. Пустой req.user не возвращает 401 — server.js
```js
} catch { req.user = {}; }
next();
```
Невалидный токен → `req.user = {}``clientId = undefined` → анонимы делят NULL-компанию. Нужно `if (!req.user.clientId) return res.status(401).send(...)`.
### 3. Race conditions (TOCTOU) — queries.js
**Три гонки из-за отсутствия транзакций:**
- **Обход лимита:** два параллельных запроса читают `cnt=14`, оба вставляют → 16 записей
- **Дубли/пересечения:** проверка `existing` и `INSERT` не в транзакции
- **Дубли компаний:** `getOrCreateCompany` — два первых запроса новой компании оба не находят, оба INSERT
**Фикс:** `BEGIN``SELECT ... FOR UPDATE` строки компании → проверки → INSERT/UPDATE → `logAudit(..., client)``COMMIT`.
### 4. audit вне транзакции — queries.js
`logAudit` использует глобальный `pool`, не клиент транзакции. При сбое: запись есть, аудита нет (или наоборот). Передавать клиент транзакции в `logAudit`.
### 5. /export без авторизации — server.js
`getExportCIDRs()` без аргументов отдаёт CIDR всех компаний без проверки `req.user`. Публичная утечка whitelist всех клиентов.
### 6. DEV_MODE не привязан к NODE_ENV — server.js
`DEV_MODE=true` в проде → все становятся `WZ01325` без auth. Добавить `&& process.env.NODE_ENV !== 'production'`.
### 7. CSRF — server.js + index.ejs
POST-формы `/add`, `/delete/:id` без CSRF-токенов. Нужен `csurf` + `<input name="_csrf">` в формах.
### 8. Обход запрещённых диапазонов через суперсеть — validators.js
Блокировка использует `isSubnetOf(normalized, blocked)` — можно обойти `/22`, содержащей запрещённый `/24` (например `192.0.2.0/22` содержит TEST-NET-1). Заменить на `overlaps(normalized, blocked)`.
---
## 🟠 Схема БД (structure)
### 9. Нет UNIQUE на активный CIDR компании — schema.sql
Дубли держатся только на коде. Добавить:
```sql
CREATE UNIQUE INDEX uq_entries_active_cidr
ON whitelist_entries(company_id, value_cidr) WHERE deleted_at IS NULL;
```
### 10. value_cidr как VARCHAR — нет проверок в БД — schema.sql
БД не валидирует формат и не ловит пересечения. Варианты:
| Уровень | Что |
|---|---|
| Минимум | `CHECK (value_cidr ~ '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$')` |
| Production | Тип `CIDR` + exclusion constraint (btree_gist) для пересечений |
### 11. FK без явного ON DELETE — schema.sql
`REFERENCES companies(id)` без указания поведения. Явно задать `ON DELETE RESTRICT`.
---
## 🟡 Средние (production-hardening)
| # | Где | Что |
|---|---|---|
| 12 | server.js | Нет **helmet** — X-Frame-Options, CSP, HSTS отсутствуют |
| 13 | server.js | Нет **rate-limit** на `/add`, `/delete`, `/export` |
| 14 | server.js | `express.urlencoded` без `limit` — DoS большими телами |
| 15 | server.js | Нет глобального error-handler middleware |
| 16 | server.js | `/healthz` под auth — сломает k8s-пробу. Вынести выше |
| 17 | server.js | `?error=` в редиректе не читается в `res.render('/', ...)` — параметр молча теряется |
| 18 | server.js | Сырые `e.message` БД наружу — info leak. Маппить на дружелюбные сообщения |
| 19 | queries.js | `custom_limit = 0` игнорируется: `company.custom_limit \|\| defaultLimit``0 \|\| 15 = 15` |
| 20 | queries.js | `getOrCreateCompany` не атомарен (хотя UNIQUE на client_id спасает). Upsert: `INSERT ... ON CONFLICT` |
| 21 | queries.js | `deleteEntry` WHERE только по `id` — добавить `AND company_id = $3` для глубины защиты |
| 22 | schema.sql | `audit_log.company_id` без FK на companies (допустимо, но задокументировать) |
| 23 | schema.sql | `audit_log.action` — свободный VARCHAR. Добавить `CHECK (action IN ('CREATE','UPDATE','DELETE'))` |
| 24 | schema.sql | Индекс аудита только на company_id. Добавить `(company_id, created_at DESC)` |
| 25 | schema.sql | `updated_at` не обновляется автоматически. Добавить триггер |
| 26 | schema.sql | `SERIAL``GENERATED ALWAYS AS IDENTITY` (PG 10+) |
| 27 | schema.sql | `custom_limit` без CHECK ≥ 0 |
| 28 | validators.js | `parseInt('24abc') = 24` — глотает мусор. Проверять `^\d{1,2}$` |
| 29 | validators.js | Множественные слэши не отсекаются: `10.0.0.0/24/8` → средняя часть игнорируется |
| 30 | index.ejs | Нет client-валидации формата (ТЗ требует). Добавить `pattern` + `maxlength="18"` |
| 31 | index.ejs | `disabled` по лимиту обходится через DevTools — не баг, т.к. сервер проверяет |
| 32 | index.ejs | Инлайн-стили → `unsafe-inline` в CSP. Вынести в `.css` для строгой политики |
---
## 🟢 Безопасно (проверено)
- **SQL-инъекций нет** — все запросы параметризованы ($1, $2...)
- **XSS в EJS нет** — всё через `<%= %>`, `<%- %>` не используется
- **Изоляция компаний корректна** — `updateEntry`/`deleteEntry` проверяют `company_id`, `listEntries` фильтрует по компании
- **Сохранённого XSS через БД нет** — все поля экранируются
- **`overlaps()` формула корректна** — проверено 12 тестами, старая и новая формулы математически эквивалентны
- **Изоляция через схему БД** — записи привязаны к `company_id`, обход только через код (не схему)
- **Partial-индекс `idx_entries_active`** — правильный приём, soft-deleted не раздувают индекс
---
## Приоритет исправлений
| Порядок | Что | Блокирует |
|---|---|---|
| 1 | JWT — проверка подписи | Продакшен |
| 2 | Транзакции в createEntry/updateEntry/deleteEntry | Целостность данных |
| 3 | UNIQUE на активный CIDR в БД | Защита от гонок |
| 4 | 401 при пустом req.user | Auth |
| 5 | DEV_MODE → NODE_ENV | Безопасность прода |
| 6 | /export — авторизация | Утечка данных |
| 7 | CSRF-токены | Безопасность |
| 8 | `overlaps` вместо `isSubnetOf` в блокировке | Валидация |
| 9 | helmet + rate-limit | Production-hardening |
| 10 | Остальное (см. таблицу 🟡) | Качество |
-82
View File
@@ -1,82 +0,0 @@
# Результаты анализа HAR — nubes_login.har
> **Дата:** 2026-05-30
> **Источник:** `nubes_login.har` — запись входа в платформу Nubes
> **Пользователь:** tazet@narod.ru (Наиль Тазетдинов)
---
## Схема авторизации
```
Браузер → lk-api-gateway.ngcloud.ru/api/v1/iam/auth/login
→ keycloak.nubes.ru/realms/cloud/login-actions/authenticate
→ auth-api (выпускает JWT)
```
Платформа использует **собственный auth-api** как надстройку над Keycloak. Токен подписан `auth-api`, не Keycloak.
---
## Реальные claim-имена в JWT
```json
{
"iss": "auth-api",
"sub": "0199e325-1cdf-7cda-9319-e5302a85e291",
"ClientID": "WZ01325",
"company_id": "3e64aac6-dcfc-4082-88dc-da19c86555a5",
"company_name": "Тест",
"email": "tazet@narod.ru",
"login": "tazet@narod.ru",
"firstname": "Наиль",
"middlename": "Фарисович Тестовая учетка",
"lastname": "Тазетдинов",
"token_type": "access",
"realm_access": { "roles": null },
"resource_access": { "account": { "roles": null } },
"groups": null
}
```
---
## Выводы для IP WhiteList
### ✅ Точно известно
| Что | Значение |
|---|---|
| claim для clientId | `ClientID` (строка, например "WZ01325") |
| claim для email | `email` |
| claim для company_id | `company_id` (UUID-строка) |
| claim для company_name | `company_name` |
| Проверка подписи | JWKS от `auth-api`, НЕ `keycloak.nubes.ru` |
| Multi-company | В этом токене — одна компания (строка, не массив) |
### ❓ Ещё не известно
| Вопрос | Почему важно |
|---|---|
| Admin-признак | `roles=null`, `groups=null` — нужен пример токена админа |
| JWKS URL auth-api | Где брать публичный ключ для проверки подписи? |
| Multi-company формат | Будет ли `ClientID` массивом для пользователей с несколькими компаниями? |
| Как токен доставляется до сервиса | `Authorization: Bearer`? Или через ingress-заголовки? |
### Как это использовать сейчас
В `server.js` middleware уже близок к правильному:
```js
// Текущий код (почти правильный):
req.user = {
email: payload.email,
clientId: payload.ClientID, // ← подтверждено HAR
companyName: payload.company_name, // ← подтверждено HAR
};
```
Нужно поправить:
- `payload.company_id` → сохранять отдельно (UUID, пригодится)
- Добавить `TODO` про admin-роль
- Добавить `TODO` про JWKS проверку (сейчас base64 без подписи)
File diff suppressed because one or more lines are too long
-97
View File
@@ -1,97 +0,0 @@
# Код-ревью index.ejs — XSS, CSRF, clickjacking, client-валидация
> Дата: 2026-05-30
## 🟢 XSS — экранирование корректно
Весь динамический вывод идёт через `<%= %>`, который EJS экранирует (`&<>"'`). `<%- %>` не используется нигде. Векторы проверены:
- `<%= message %>`, `<%= error %>` — экранируются. Даже если в `error` попадёт сырая ошибка БД с `<script>`, она будет обезврежена.
- `<%= e.value_cidr %>`, `<%= e.comment %>`, `<%= e.created_by %>` (данные из БД) — экранируются.
- `<%= user.clientId %>`, `<%= user.email %>` — экранируются.
Сохранённого XSS через комментарий/email нет. Это сильная сторона шаблона.
⚠️ Единственный нюанс: `action="/delete/<%= e.id %>"``e.id` идёт в атрибут URL. Так как это integer из БД (SERIAL), инъекция невозможна. Но если тип когда-нибудь станет строковым — атрибутный контекст потребует особой осторожности. Сейчас безопасно.
## 🔴 CSRF — формы без токена (критично)
```html
<form method="POST" action="/add">
<form method="POST" action="/delete/<%= e.id %>" ...>
```
Ни одна форма не содержит CSRF-токена. Обе меняют состояние. Сторонний сайт может авто-сабмитить POST на `/add`/`/delete/:id`. Зеркалит находку из ревью server.js. Фикс — пробросить токен из middleware (`csurf`) и в каждой форме:
```html
<form method="POST" action="/add">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
...
</form>
<form method="POST" action="/delete/<%= e.id %>" style="display:inline" onsubmit="return confirm('Удалить запись?')">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<button class="btn btn-danger">Удалить</button>
</form>
```
(Требует прокидывания `csrfToken` в `res.render` во всех роутах server.js.)
## 🔴 Clickjacking — нет защиты фрейминга
Шаблон с кнопками «Удалить» можно встроить в `<iframe>` на фишинговом сайте и подложить под клик (UI redress). В самом EJS защиты нет — нужны заголовки на стороне server.js (`helmet``X-Frame-Options: DENY` / CSP `frame-ancestors 'none'`). Дублирует находку из ревью server.js. На уровне шаблона можно добавить CSP через meta (слабее заголовка, но лучше чем ничего):
```html
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'; default-src 'self'; style-src 'self' 'unsafe-inline'">
```
⚠️ `style-src 'unsafe-inline'` потребуется из-за инлайн-`<style>` и inline-атрибутов `style="..."` в header/кнопках — это ослабляет CSP. По-хорошему вынести стили в отдельный `.css` файл и убрать `unsafe-inline`.
## 🟡 disabled-поля обходятся через DevTools (это и есть главная дыра валидации)
```html
<input name="value" ... <%= used >= limit ? 'disabled' : '' %>>
<button ... <%= used >= limit ? 'disabled' : '' %>>
```
`disabled` — только UX. Атакующий через DevTools снимает атрибут и шлёт POST `/add` сверх лимита. ЭТО НЕ УЯЗВИМОСТЬ ШАБЛОНА, пока сервер проверяет лимит — а он проверяет (`createEntry`). Вывод: клиентский `disabled` не является защитой и не должен ею считаться; настоящая защита — серверная проверка лимита (она есть, но уязвима к гонке — см. ревью queries.js). Шаблон тут корректен ровно при условии серверной проверки.
## 🟡 Нет client-side валидации (несоответствие ТЗ)
ТЗ требует клиентскую валидацию формата IPv4/CIDR. Сейчас только `required` и серверная проверка. Пользователь узнаёт об ошибке только после round-trip. Добавить `pattern` для базовой проверки + JS для маски /22/32:
```html
<input name="value"
pattern="^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$"
title="IPv4 или CIDR, например 203.0.113.0/24"
placeholder="Например: 203.0.113.10 или 203.0.113.0/24"
required <%= used >= limit ? 'disabled' : '' %>>
```
`pattern` — только формат; диапазон маски (/22–/32) и host-биты всё равно валидирует сервер (`validators.js`). Это UX-улучшение, не замена серверной проверки.
## 🟡 maxlength только на comment, не на value
`comment` имеет `maxlength="255"` (совпадает со схемой VARCHAR(255) — хорошо). У `value` нет `maxlength` — стоит добавить `maxlength="18"` под `VARCHAR(18)`, чтобы не слать заведомо длинное и для согласованности.
## 🟢 Утечка чужих данных — нет
В шаблоне выводятся только `user.clientId`/`user.email` (свои) и `entries` (своей компании, отфильтрованы по company_id в `listEntries`). Данных других компаний нет. Изоляция на уровне шаблона соблюдена (зависит от корректной фильтрации в queries.js — там она есть).
## 🟡 onsubmit confirm — не защита, но ок
`onsubmit="return confirm(...)"` легко обходится, но это UX-подтверждение, не security-контроль. Приемлемо.
## 🟡 favicon/иконка — внешних ресурсов нет
Все ресурсы локальные (`/favicon.png`, инлайн SVG, инлайн CSS). Нет внешних CDN → меньше поверхность для supply-chain. Хорошо. Обратная сторона — инлайн-стили мешают строгой CSP (см. выше).
---
**Итог:**
1. 🟢 XSS нет — всё через `<%= %>`, `<%- %>` не используется. Главная сильная сторона.
2. 🔴 CSRF-токенов в формах нет — добавить `_csrf` в `/add` и `/delete` (+ middleware в server.js).
3. 🔴 Clickjacking — защита только заголовками (helmet в server.js); опционально CSP-meta.
4. 🟡 `disabled` по лимиту обходится через DevTools — не баг шаблона при условии серверной проверки (она есть).
5. 🟡 Нет client-валидации формата (ТЗ требует) — добавить `pattern` + `maxlength` на `value`.
6. 🟡 Инлайн-стили вынудят `unsafe-inline` в CSP — вынести в отдельный .css для строгой политики.
Шаблон по XSS написан правильно; основные пробелы — CSRF и clickjacking (закрываются в server.js) и отсутствие клиентской валидации из ТЗ.
-133
View File
@@ -1,133 +0,0 @@
# Код-ревью queries.js — гонки, транзакции, безопасность
> Дата: 2026-05-30
## 🔴 Race condition 1 — обход лимита (TOCTOU, критично)
`createEntry`: между `SELECT COUNT(*)` (проверка лимита) и `INSERT` нет транзакции и блокировки. Два параллельных запроса от одной компании оба прочитают `cnt = 14`, оба пройдут проверку `cnt >= 15`, оба вставят запись → 16 записей при лимите 15. То же самое позволяет вставить две пересекающиеся/дублирующие записи одновременно (проверка `existing` тоже вне транзакции).
Фикс — обернуть всю операцию в транзакцию с блокировкой строки компании (`SELECT ... FOR UPDATE` сериализует параллельные вставки в рамках одной компании):
```js
async function createEntry(companyId, rawValue, comment, userEmail) {
const { cidr, wasNormalized } = validate(rawValue);
const client = await pool.connect();
try {
await client.query('BEGIN');
// блокируем строку компании — параллельные createEntry этой компании встают в очередь
const company = (await client.query(
'SELECT * FROM companies WHERE id = $1 FOR UPDATE', [companyId]
)).rows[0];
if (!company) throw new Error('Компания не найдена');
const limit = await getLimit(company);
const cnt = (await client.query(
'SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL',
[companyId]
)).rows[0].c;
if (cnt >= limit) throw new Error(`Лимит исчерпан: ${cnt} из ${limit}`);
const existing = (await client.query(
'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL',
[companyId]
)).rows;
for (const row of existing) {
if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует');
if (overlaps(cidr, row.value_cidr))
throw new Error(`Пересечение с существующей записью ${row.value_cidr}`);
}
const res = await client.query(
`INSERT INTO whitelist_entries (company_id, value_cidr, comment, created_by)
VALUES ($1, $2, $3, $4) RETURNING *`,
[companyId, cidr, comment || null, userEmail]
);
await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id, client);
await client.query('COMMIT');
return { entry: res.rows[0], wasNormalized };
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
```
## 🔴 Race condition 2 — то же в updateEntry
`updateEntry` имеет идентичную проблему: проверка пересечений (`existing`) и `UPDATE` не в транзакции. Параллельное обновление двух записей в пересекающиеся CIDR пройдёт обе проверки. Обернуть так же: `BEGIN``SELECT ... FOR UPDATE` строки компании → проверки → `UPDATE``logAudit(...,client)``COMMIT`/`ROLLBACK`.
## 🔴 Race condition 3 — getOrCreateCompany (дубли компаний)
`getOrCreateCompany`: между `SELECT` и `INSERT` нет защиты. Два первых запроса новой компании оба не найдут строку и оба сделают `INSERT`. Спасает только `UNIQUE` на `client_id` в схеме (второй упадёт), но ошибка вылетит наружу некрасиво. Фикс — атомарный upsert:
```js
async function getOrCreateCompany(clientId, companyName) {
const res = await pool.query(
`INSERT INTO companies (client_id, name) VALUES ($1, $2)
ON CONFLICT (client_id) DO UPDATE SET name = COALESCE(companies.name, EXCLUDED.name)
RETURNING *`,
[clientId, companyName || clientId]
);
return res.rows[0];
}
```
## 🟡 audit_log пишется вне транзакции
`logAudit` использует глобальный `pool`, а не клиента транзакции. Если INSERT записи прошёл, а logAudit упал — запись есть, аудита нет (или наоборот при будущих изменениях). Аудит обязателен по ТЗ. Передавать клиента транзакции:
```js
async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId, db = pool) {
await db.query(
`INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id)
VALUES ($1, $2, $3, $4, $5, $6)`,
[userEmail, companyId, action, oldValue, newValue, entryId || null]
);
}
```
## 🟡 deleteEntry — UPDATE без company_id в WHERE
```js
await pool.query(
'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2',
[userEmail, entryId]
);
```
`old` уже проверен по `company_id`, поэтому изоляция сейчас не нарушается. Но WHERE по одному `id` хрупкий — при рефакторинге легко потерять привязку. Дублировать company_id в WHERE для глубины защиты:
```js
await pool.query(
'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2 AND company_id = $3',
[userEmail, entryId, companyId]
);
```
Также deleteEntry не в транзакции с logAudit — обернуть аналогично create/update.
## 🟡 getLimit — custom_limit = 0 игнорируется
```js
return company.custom_limit || defaultLimit;
```
Если админ задал `custom_limit = 0` (запретить компании добавлять), `0 || 15` вернёт 15. ТЗ разрешает снижать лимит. Фикс:
```js
return company.custom_limit != null ? company.custom_limit : defaultLimit;
```
## 🟢 SQL-инъекций нет
Все запросы параметризованы ($1, $2...). Конкатенации с пользовательским вводом нет. `listEntries`/`getAudit` строят SQL из булевых флагов, не из ввода — безопасно.
## 🟢 Изоляция по company_id
`updateEntry` и `deleteEntry` проверяют `company_id` при выборке `old` — пользователь компании А не затронет записи компании Б. Корректно (но см. замечание по deleteEntry WHERE).
---
**Итог:** SQL-инъекций и утечек между компаниями нет. Главная проблема — отсутствие транзакций: 3 эксплуатируемые гонки (обход лимита, дубли/пересечения, дубли компаний) + риск рассинхрона аудита. Все чинятся обёрткой в транзакцию с `FOR UPDATE` и передачей клиента в logAudit. Плюс мелкий баг с `custom_limit = 0`.
-141
View File
@@ -1,141 +0,0 @@
# Код-ревью schema.sql — индексы, constraint'ы, FK, типы
> Дата: 2026-05-30
## 🔴 Нет уникального constraint на активный CIDR компании
Дубликаты предотвращаются только в коде (`createEntry`), а это уязвимо к гонке (см. ревью queries.js — TOCTOU). БД должна гарантировать уникальность активного адреса в рамках компании независимо от кода:
```sql
CREATE UNIQUE INDEX IF NOT EXISTS uq_entries_active_cidr
ON whitelist_entries(company_id, value_cidr) WHERE deleted_at IS NULL;
```
Это превращает существующий `idx_entries_active` в уникальный (можно заменить им) — параллельные INSERT одинакового CIDR упадут на втором, гонка закрывается на уровне БД. Пересечения (overlaps) так не закрыть — для них нужен `inet`/GiST (см. ниже) или транзакция.
## 🔴 value_cidr хранится как VARCHAR — нет валидации и пересечений на уровне БД
```sql
value_cidr VARCHAR(18) NOT NULL,
```
`VARCHAR(18)` хранит произвольную строку — БД не проверяет, что это валидный CIDR, и не умеет искать пересечения. Production-вариант — нативный тип `cidr`:
```sql
value_cidr CIDR NOT NULL,
```
Преимущества: БД отвергает мусор; операторы `&&` (overlaps), `<<=` (subnet); можно сделать exclusion constraint на пересечения внутри компании:
```sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE whitelist_entries
ADD CONSTRAINT excl_entries_overlap
EXCLUDE USING gist (company_id WITH =, value_cidr inet_ops WITH &&)
WHERE (deleted_at IS NULL);
```
Это закрывает гонку пересечений (RC №2 из ревью queries.js) на уровне БД. Если тип менять не хотите — оставить VARCHAR, но тогда уникальность/пересечения держатся только на транзакциях в коде. Минимум — CHECK на формат:
```sql
ALTER TABLE whitelist_entries
ADD CONSTRAINT chk_cidr_format CHECK (value_cidr ~ '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$');
```
## 🔴 FK без ON DELETE / нет каскада
```sql
company_id INTEGER NOT NULL REFERENCES companies(id),
```
Поведение по умолчанию — `NO ACTION`: удалить компанию нельзя, пока есть записи. Для сервиса с soft-delete это, скорее, правильно (компании не удаляются физически). Но это надо сделать осознанно: явно указать `ON DELETE RESTRICT` (документирует намерение) либо `ON DELETE CASCADE`, если компании реально удаляются. Сейчас умолчание неявное.
## 🟡 audit_log.company_id без FK и без типизации действий
```sql
company_id INTEGER NOT NULL,
action VARCHAR(32) NOT NULL,
```
`company_id` в audit_log не ссылается на `companies` — допустимо (аудит должен переживать удаление компании), но тогда стоит это зафиксировать комментарием. `action` — свободный VARCHAR, можно записать что угодно. Ограничить:
```sql
ALTER TABLE audit_log
ADD CONSTRAINT chk_action CHECK (action IN ('CREATE','UPDATE','DELETE'));
```
`entry_id` тоже без FK — ок (запись может быть hard-удалена в будущем, аудит сохраняется).
## 🟡 Индекс аудита недостаточен для типичных запросов
```sql
CREATE INDEX idx_audit_company ON audit_log(company_id);
```
Аудит почти всегда смотрят «по компании, свежие сверху». Нужен составной с временем:
```sql
CREATE INDEX IF NOT EXISTS idx_audit_company_time
ON audit_log(company_id, created_at DESC);
```
## 🟡 Нет автообновления updated_at
`updated_at` в companies имеет DEFAULT NOW(), но при UPDATE не меняется автоматически — код должен сам выставлять. Для надёжности — триггер:
```sql
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_companies_updated
BEFORE UPDATE ON companies
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
```
## 🟡 SERIAL вместо IDENTITY
`SERIAL` — легаси-приём. Для нового кода предпочтительнее:
```sql
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
```
Не критично, но это современный стандарт PG 10+ (чище права на sequence, нельзя случайно вставить id вручную).
## 🟡 custom_limit без CHECK на неотрицательность
```sql
custom_limit INTEGER DEFAULT NULL,
```
Можно записать отрицательный лимит. Добавить:
```sql
ALTER TABLE companies
ADD CONSTRAINT chk_custom_limit CHECK (custom_limit IS NULL OR custom_limit >= 0);
```
(Связано с багом `custom_limit = 0` из ревью queries.js — на уровне БД 0 разрешён, в коде игнорируется.)
## 🟡 comment/created_by — длины
`created_by VARCHAR(255)` под email — ок. `comment VARCHAR(255)` — приемлемо, но если ТЗ не ограничивает комментарий — рассмотреть TEXT. Не критично.
## 🟢 Изоляция через БД
Структурно обойти изоляцию нельзя: записи привязаны к `company_id`, утечка возможна только через код (запрос без фильтра company_id — см. `/export` в ревью server.js), не через схему.
## 🟢 Партиal-индекс idx_entries_active
Правильный приём — индекс только по активным записям, soft-deleted не раздувают индекс. Хорошо.
---
**Итог:**
1. 🔴 Добавить UNIQUE на активный (company_id, value_cidr) — закрывает гонку дублей на уровне БД.
2. 🔴 Рассмотреть тип `CIDR` + exclusion constraint (btree_gist) — закрывает гонку пересечений в БД; иначе минимум CHECK на формат.
3. 🔴 Явно задать ON DELETE для FK company_id.
4. 🟡 CHECK на action, на custom_limit ≥ 0; составной индекс аудита (company_id, created_at DESC); FK-политику аудита задокументировать.
5. 🟡 Триггер updated_at; перейти на IDENTITY вместо SERIAL.
Главное: текущая схема перекладывает уникальность и проверку пересечений целиком на код, который к ним уязвим в гонках. Перенос этих гарантий в БД (UNIQUE + exclusion/CHECK) — основной production-апгрейд.
-97
View File
@@ -1,97 +0,0 @@
# Код-ревью server.js — auth, CSRF, XSS, заголовки
> Дата: 2026-05-30
## 🔴 Auth bypass 1 — отсутствие проверки подписи JWT (критично)
```js
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
```
Это не аутентификация — это просто декодирование base64 payload без проверки подписи. Любой может прислать самодельный токен `Bearer xxx.<base64 любого JSON>.yyy` и стать любой компанией. `ClientID`, `company_id` полностью подконтрольны атакующему → полный обход изоляции компаний, доступ к чужим whitelist, экспорт. Это та самая дыра, ради которой нужен JWKS/проверка подписи (см. отдельный prompt-opus-auth). До внедрения проверки подписи продакшен поднимать нельзя.
Минимум: `jose.jwtVerify(token, JWKS, { issuer: 'auth-api' })` с кэшированием ключей, и брать payload только из верифицированного результата. Также `split('.')[1]` упадёт на токене без точек → попадёт в `catch``req.user = {}`.
## 🔴 Auth bypass 2 — пустой req.user не блокирует доступ
```js
} catch { req.user = {}; }
next();
```
При невалидном токене `req.user = {}` и запрос идёт дальше. В `/` тогда `clientId = undefined``getOrCreateCompany(undefined, undefined)` создаст/найдёт компанию с `client_id = NULL`. Все анонимы делят одну «нулевую» компанию, видят и редактируют её whitelist. Нет ни одного `return res.status(401)`. Фикс — после catch и для не-DEV пути: если нет `req.user.clientId``return res.status(401).send('Unauthorized')`.
## 🔴 DEV_MODE — риск включения в проде
```js
const DEV = process.env.DEV_MODE === 'true';
...
if (DEV) { req.user = { ... clientId: 'WZ01325' ... }; return next(); }
```
Если `DEV_MODE=true` случайно попадёт в прод-конфиг — полный обход аутентификации, все становятся `WZ01325`. Нет защиты «DEV только не в production». Добавить страховку: `const DEV = process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production';` и логировать предупреждение при старте, если DEV активен.
## 🔴 CSRF — все POST-формы без токенов (критично)
`/add`, `/delete/:id` — обычные form-POST, меняют состояние, без CSRF-токена. В проде аутентификация по cookie/сессии (а не по заголовку Authorization вручную) ⇒ сторонний сайт может отправить `<form action="https://white.../delete/123" method=POST>` и удалить чужие записи. Если же токен реально приходит только в заголовке Authorization (не в cookie), CSRF слабее — но форма в браузере не может сама проставить Authorization, значит модель аутентификации в браузере вообще не работает с текущим кодом. Это надо прояснить (см. questions.md). В любом случае при cookie-сессии нужен CSRF-токен (`csurf` или double-submit) на все POST.
## 🟡 XSS через ?error= в редиректе
```js
res.redirect('/?error=' + encodeURIComponent(e.message));
```
Сам редирект экранирует. Уязвимость — в шаблоне: если `index.ejs` выводит `error` через `<%- %>` (не экранируя) — рефлексивный XSS. По ревью ejs вывод идёт через `<%= %>`, так что сейчас безопасно. Но `e.message` может содержать сырой текст ошибки БД — нежелательно показывать пользователю (info leak). Маппить на дружелюбные сообщения, не отдавать `e.message` БД наружу.
Кроме того `/?error=` читается из query, но в роуте `/` параметр `req.query.error` вообще не прокидывается в render (`error: null`) — то есть редирект с `?error=` ничего не покажет. Несоответствие: либо читать `req.query.error`, либо убрать. Если читать — обязательно только экранированный вывод.
## 🟡 Обработка ошибок — каскад в /add
```js
} catch (e) {
const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null);
const entries = company ? await q.listEntries(company.id).catch(() => []) : [];
...
```
В catch-ветке снова дёргается БД (3 запроса). Если БД легла — все упадут в `.catch(() => ...)` и пользователь увидит исходную ошибку с пустым списком — приемлемо, но шумно. Главное: нет глобального error-handler middleware (`app.use((err, req, res, next) => ...)`) — необработанный промис в любом роуте уронит ответ висящим. Добавить финальный error middleware + `process.on('unhandledRejection')`.
## 🟡 Нет helmet / security-заголовков
Отсутствуют `X-Frame-Options`/CSP (clickjacking), `X-Content-Type-Options: nosniff`, `Referrer-Policy`, HSTS. Добавить `helmet()` сразу после создания app. Особенно `frame-ancestors`/`X-Frame-Options: DENY` — UI с формами удаления уязвим к clickjacking.
## 🟡 Нет rate-limit
`/add`, `/delete`, `/export` без ограничения частоты. Можно засыпать INSERT-ами/экспортом. Добавить `express-rate-limit` на мутирующие роуты и на `/export`.
## 🟡 /export — нет авторизации и изоляции (важно по ТЗ)
```js
app.get('/export', async (req, res) => {
const cidrs = await q.getExportCIDRs();
```
`getExportCIDRs()` без аргументов — отдаёт CIDR ВСЕХ компаний всем подряд, без проверки `req.user`, без фильтра по компании. Это утечка whitelist всех клиентов. По ТЗ экспорт должен быть либо служебным (ограничен по IP/токену), либо в рамках компании. Сейчас — публичный дамп всех адресов. Требует решения из questions.md (IP-ограничение/служебный токен), но в текущем виде — критичная утечка.
## 🟡 /healthz выше auth — ок, но раскрывает «OK» только
`/healthz` объявлен после middleware (значит проходит через auth). При DEV ок; в проде healthz будет требовать токен → проба готовности k8s упадёт. Вынести `/healthz` ВЫШЕ auth middleware.
## 🟡 Тело запроса без лимита размера
`express.urlencoded({ extended: true })` без `limit`. DoS большими телами. Поставить `limit: '32kb'`.
## 🟢 SSRF/path traversal
`express.static` на фиксированную папку, шаблоны фиксированы — обхода нет.
---
**Итог (по приоритету):**
1. 🔴 JWT без проверки подписи → полный обход auth и изоляции — блокер прода.
2. 🔴 Пустой `req.user = {}` не возвращает 401 → анонимы делят NULL-компанию.
3. 🔴 `/export` без авторизации отдаёт CIDR всех компаний — утечка.
4. 🔴 DEV_MODE без привязки к NODE_ENV — риск обхода auth в проде.
5. 🔴 Нет CSRF на POST-формах.
6. 🟡 Нет helmet, rate-limit, лимита тела, глобального error-handler.
7. 🟡 `/healthz` под auth (сломает k8s-пробу), `?error=` не читается в `/`, сырые ошибки БД наружу.
-67
View File
@@ -1,67 +0,0 @@
# Ответ Claude Opus 4 — код-ревью validators.js
> Дата: 2026-05-30
## 🔴 Баг 1 — запрещённые диапазоны обходятся бо́льшей подсетью (критично)
Проверка блокировки использует `isSubnetOf(normalized, blocked)` — то есть «запись внутри запрещённого диапазона». Но не проверяется обратное направление: **запись, которая САМА содержит запрещённый диапазон**.
Поскольку ТЗ разрешает маски вплоть до `/22`, а многие запрещённые блоки — это `/24`, их можно обойти суперсетью:
```
Вход: 192.0.2.0/22
→ нормализуется в 192.0.0.0/22 (диапазон 192.0.0.0 192.0.3.255)
```
Этот `/22` **содержит** TEST-NET-1 (`192.0.2.0/24`) и IANA special (`192.0.0.0/24`), но `isSubnetOf` вернёт `false` (родитель не внутри ребёнка) → запись пройдёт валидацию. То же с `198.18.0.0/15` (Benchmarking) и др.
**Фикс** — использовать пересечение вместо вложенности:
```js
for (const blocked of BLOCKED_RANGES) {
if (overlaps(normalized, blocked)) {
throw new Error(`Диапазон ${normalized} пересекается с запрещённым (${blocked})`);
}
}
```
`overlaps` ловит оба направления (и когда запись внутри блока, и когда блок внутри записи). `isSubnetOf` после этого фикса в блокировке не нужен.
---
## 🟡 Замечание 2 — parseInt глотает мусор в маске
```js
const mask = parseInt(maskStr, 10);
```
`10.0.0.0/24abc``parseInt('24abc')` = `24`, запись принимается как `/24`. Строгая проверка:
```js
if (!/^\d{1,2}$/.test(maskStr)) throw new Error('Некорректная маска');
const mask = parseInt(maskStr, 10);
```
---
## 🟡 Замечание 3 — множественные слэши не отсекаются
`10.0.0.0/24/8`.split('/') → `['10.0.0.0','24','8']`, третий элемент молча игнорируется. Добавить проверку числа частей:
```js
const parts = cidr.split('/');
if (parts.length !== 2) throw new Error('Некорректный формат CIDR');
const [addr, maskStr] = parts;
```
---
## 🟢 Не баги (проверено)
- **`overlaps`** — формула `a.start <= b.end && b.start <= a.start || b.start <= a.end && a.start <= b.start` хоть и обфусцирована, **математически эквивалентна** стандартной `a.start <= b.end && b.start <= a.end`. Работает корректно. Можно упростить для читаемости, но это не дефект.
- **Битовая арифметика** с `<<8` уходит в знаковый 32-бит, но финальный `>>> 0` всё исправляет. Корректно.
- **Маска `/0`**, `1 << (32-mask)` — безопасно, т.к. `mask` ограничен 2232.
---
**Итог:** один настоящий эксплуатируемый баг (#1 — обход TEST-NET/special через `/22`), два мелких по строгости парсинга. Главное — поправить блокировку на `overlaps`.
-49
View File
@@ -1,49 +0,0 @@
# Промпт для Claude Opus 4 — архитектура OIDC/JWT для внешнего auth-api
## Контекст (не анализируй)
Делаем микросервис на Node.js + Express. Он стоит ЗА общим auth-api платформы. Платформа сама не наша, мы не можем менять Keycloak.
**Схема авторизации платформы:**
1. Браузер → lk-api-gateway → Keycloak (логин)
2. После логина платформа обменивает code на токен через СВОЙ auth-api
3. auth-api выпускает JWT с `"iss": "auth-api"` (не Keycloak!)
4. Фронтенд платформы хранит access_token в localStorage и шлёт `Authorization: Bearer <token>` к своему бэкенду
**Нам неизвестно:**
- JWKS URL auth-api (публичный ключ для проверки подписи)
- Как именно фронтенд платформы будет вызывать НАШ сервис (прямой запрос браузера с токеном? Или через их API-гейтвей?)
- Есть ли у нас доступ к этому auth-api или только к самому JWT
**Реальный JWT (из HAR трафика):**
```json
{
"iss": "auth-api",
"sub": "0199e325-1cdf-7cda-9319-e5302a85e291",
"ClientID": "WZ01325",
"company_id": "3e64aac6-dcfc-4082-88dc-da19c86555a5",
"company_name": "Тест",
"email": "tazet@narod.ru",
"token_type": "access",
"realm_access": { "roles": null },
"resource_access": { "account": { "roles": null } },
"groups": null
}
```
## Что нужно
Предложи стратегию проверки токенов в нашем сервисе. Мы не знаем JWKS URL auth-api и не имеем к нему доступа (пока). Нужно найти золотую середину между «вообще не проверяем подпись» и «требуем JWKS которого нет».
Конкретные вопросы:
1. Если JWKS недоступен — что проверять ВМЕСТО подписи? (exp, iss, audience?)
2. Может ли наш сервис валидировать iss='auth-api' без криптографии?
3. Стоит ли делать промежуточный вариант: проверять exp+iss сейчас, а JWKS добавить когда дадут URL?
4. Как защититься от подделки токена если подпись не проверяется?
5. Нужен ли нам client_secret/shared secret с auth-api?
Ограничения:
- Не предлагай «спросить у команды платформы» — мы и так спросим, но ответа пока нет
- Не предлагай поднять свой Keycloak
- Только практические варианты, которые можно закодить сейчас
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов
-245
View File
@@ -1,245 +0,0 @@
# Промпт для Claude Opus 4 — XSS, CSRF, безопасность index.ejs
## Контекст (не анализируй)
Node.js + Express + EJS. Шаблон серверного рендеринга. Данные приходят из БД (email пользователя, CIDR, комментарий) и из query-параметров (error, message). EJS по умолчанию экранирует `<%= ... %>`, НО не экранирует `<%- ... %>`. В этом шаблоне `<%-` не используется.
## Что нужно
Ниже полный `index.ejs`. Найди:
- XSS-векторы (экранирование, query-параметры в URL)
- CSRF (нет токена в формах)
- Clickjacking (отсутствие X-Frame-Options / CSP frame-ancestors)
- Утечка данных (видны ли clientId/email других компаний?)
- Client-side валидация (отсутствует, хотя ТЗ требует)
- Проблемы с disabled-полями (можно ли обойти через DevTools?)
Ограничения:
- Не предлагай менять стек/фреймворк
- Только конкретные строки с исправлениями
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, чистый текст
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Белые списки IP — Nubes</title>
<link rel="icon" href="/favicon.png" type="image/png">
<style>
:root {
--bg: #f5f5f5;
--card: #ffffff;
--text: #1a1a1a;
--muted: #6b7280;
--border: #d1d5db;
--grey-light: #f3f4f6;
--blue: #2563eb;
--blue-h: #1d4ed8;
--red: #dc2626;
--red-h: #b91c1c;
--green: #16a34a;
--amber: #d97706;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
}
.page { max-width: 1100px; margin: 1.5rem auto; padding: 0 1rem; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,.04);
margin-bottom: 1rem;
overflow: hidden;
}
.card-header {
background: var(--grey-light);
padding: .75rem 1rem;
font-weight: 600;
font-size: 1rem;
border-bottom: 1px solid var(--border);
}
.card-body { padding: 1rem; }
.alert { padding: .75rem 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: .9rem; }
.alert-ok { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
.alert-err { background: #fecaca; color: #991b1b; border: 1px solid #fca5a5; }
.alert-warn{ background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
.stats { display: flex; gap: 2rem; }
.stat { text-align: center; }
.stat .num { font-size: 1.6rem; font-weight: 700; }
.stat .lbl { font-size: .8rem; color: var(--muted); }
.stat.full .num { color: var(--red); }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: .75rem 1rem;
align-items: end;
}
.field { display: flex; flex-direction: column; gap: .25rem; }
.field label {
font-size: .8rem;
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .5px;
}
.field input, .field textarea {
padding: .5rem .75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: .9rem;
outline: none;
transition: border .15s;
background: #fff;
}
.field input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
.btn {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .5rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: .85rem;
font-weight: 500;
background: #fff;
cursor: pointer;
transition: background .15s;
white-space: nowrap;
}
.btn:hover { background: var(--grey-light); }
.btn-primary { background: var(--blue); color: #fff; border-color: var(--blue); }
.btn-primary:hover { background: var(--blue-h); }
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; border-color: #93c5fd; }
.btn-danger { color: var(--red); border-color: var(--red); }
.btn-danger:hover { background: #fecaca; }
.table-wrap {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
table { width: 100%; border-collapse: collapse; font-size: .85rem; }
th {
text-align: left;
padding: .5rem .75rem;
background: var(--grey-light);
color: var(--muted);
font-weight: 500;
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .5px;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
}
th:last-child { border-right: none; }
td {
padding: .5rem .75rem;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
}
td:last-child { border-right: none; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #f8fafc; }
code { background: #f1f5f9; padding: .15rem .4rem; border-radius: 3px; font-size: .9em; }
.empty { text-align: center; color: var(--muted); padding: 2rem; }
.text-center { text-align: center; }
.mt-3 { margin-top: 1rem; }
</style>
</head>
<body>
<header style="background:#fff;border-bottom:1px solid var(--border);padding:0 1.5rem;height:48px;display:flex;align-items:center;gap:.75rem;font-size:.9rem;color:var(--muted);">
<svg width="130" height="28" viewBox="330 228 311 69" style="display:block;">...</svg>
<span>|</span>
<span style="font-weight:500;color:var(--text);">Белые списки IP</span>
</header>
<div class="page">
<% if (message) { %><div class="alert <%= wasNormalized ? 'alert-warn' : 'alert-ok' %>"><%= message %></div><% } %>
<% if (error) { %><div class="alert alert-err"><%= error %></div><% } %>
<div class="card">
<div class="card-body">
<div class="stats">
<div class="stat <%= used >= limit ? 'full' : '' %>">
<div class="num"><%= used %> / <%= limit %></div>
<div class="lbl">записей</div>
</div>
<div class="stat">
<div class="num"><%= user.clientId %></div>
<div class="lbl">компания</div>
</div>
<div class="stat">
<div class="num"><%= user.email %></div>
<div class="lbl">пользователь</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">Добавить адрес</div>
<div class="card-body">
<form method="POST" action="/add">
<div class="form-grid">
<div class="field">
<label>IPv4 адрес или подсеть CIDR</label>
<input name="value" placeholder="Например: 203.0.113.10 или 203.0.113.0/24" required <%= used >= limit ? 'disabled' : '' %>>
</div>
<div class="field">
<label>Комментарий</label>
<input name="comment" placeholder="Необязательно" maxlength="255">
</div>
<button class="btn btn-primary" type="submit" <%= used >= limit ? 'disabled' : '' %> style="align-self:end">
<%= used >= limit ? 'Лимит исчерпан' : 'Добавить' %>
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">Доверенные адреса</div>
<% if (entries.length === 0) { %>
<div class="empty">Нет добавленных адресов</div>
<% } else { %>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Адрес / Подсеть</th>
<th>Комментарий</th>
<th>Добавил</th>
<th>Дата</th>
<th class="text-center">Действие</th>
</tr>
</thead>
<tbody>
<% entries.forEach(e => { %>
<tr>
<td><code><%= e.value_cidr %></code></td>
<td><%= e.comment || '—' %></td>
<td><%= e.created_by %></td>
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) %></td>
<td class="text-center">
<form method="POST" action="/delete/<%= e.id %>" style="display:inline" onsubmit="return confirm('Удалить запись?')">
<button class="btn btn-danger">Удалить</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
</body>
</html>
```
File diff suppressed because it is too large Load Diff
-168
View File
@@ -1,168 +0,0 @@
# Промпт для Claude Opus 4 — гонки, транзакции, безопасность в queries.js
## Контекст (не анализируй)
Node.js + Express + PostgreSQL (pg pool). Микросервис IP WhiteList. Клиенты создают до 15 доверенных IPv4/CIDR. Многоарендность (изоляция по company_id). Аудит всех изменений обязателен.
## Что нужно
Ниже полный код `queries.js`. Найди:
- Race conditions (TOCTOU между проверкой лимита и INSERT)
- Отсутствие транзакций там где они нужны
- SQL-инъекции
- Ошибки изоляции (может ли пользователь компании А затронуть записи компании Б?)
- Проблемы с audit_log (пишется ли при ошибках?)
Ограничения:
- Не предлагай менять стек
- Только конкретные строки с исправлениями
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, без ссылок. Чистый текст.
```js
const { pool } = require('./db');
const { validate, overlaps } = require('./validators');
// ── Companies ──
async function getOrCreateCompany(clientId, companyName) {
let res = await pool.query('SELECT * FROM companies WHERE client_id = $1', [clientId]);
if (res.rows.length > 0) return res.rows[0];
res = await pool.query(
'INSERT INTO companies (client_id, name) VALUES ($1, $2) RETURNING *',
[clientId, companyName || clientId]
);
return res.rows[0];
}
async function getLimit(company) {
const defaultLimit = parseInt(process.env.DEFAULT_LIMIT, 10) || 15;
return company.custom_limit || defaultLimit;
}
// ── Entries ──
async function listEntries(companyId, includeDeleted = false) {
let sql = 'SELECT * FROM whitelist_entries WHERE company_id = $1';
if (!includeDeleted) sql += ' AND deleted_at IS NULL';
sql += ' ORDER BY created_at DESC';
return (await pool.query(sql, [companyId])).rows;
}
async function createEntry(companyId, rawValue, comment, userEmail) {
const { cidr, wasNormalized } = validate(rawValue);
// Проверка лимита
const company = (await pool.query('SELECT * FROM companies WHERE id = $1', [companyId])).rows[0];
const limit = await getLimit(company);
const cnt = (await pool.query(
'SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL',
[companyId]
)).rows[0].c;
if (cnt >= limit) throw new Error(`Лимит исчерпан: ${cnt} из ${limit}`);
// Проверка дубликатов и пересечений
const existing = (await pool.query(
'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL',
[companyId]
)).rows;
for (const row of existing) {
if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует');
if (overlaps(cidr, row.value_cidr))
throw new Error(`Пересечение с существующей записью ${row.value_cidr}`);
}
const res = await pool.query(
`INSERT INTO whitelist_entries (company_id, value_cidr, comment, created_by)
VALUES ($1, $2, $3, $4) RETURNING *`,
[companyId, cidr, comment || null, userEmail]
);
// Аудит
await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id);
return { entry: res.rows[0], wasNormalized };
}
async function updateEntry(entryId, companyId, rawValue, comment, userEmail) {
const old = (await pool.query(
'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL',
[entryId, companyId]
)).rows[0];
if (!old) throw new Error('Запись не найдена');
const { cidr, wasNormalized } = validate(rawValue);
const existing = (await pool.query(
'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL AND id <> $2',
[companyId, entryId]
)).rows;
for (const row of existing) {
if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует');
if (overlaps(cidr, row.value_cidr))
throw new Error(`Пересечение с существующей записью ${row.value_cidr}`);
}
const res = await pool.query(
`UPDATE whitelist_entries SET value_cidr = $1, comment = $2, updated_by = $3, updated_at = NOW()
WHERE id = $4 AND company_id = $5 RETURNING *`,
[cidr, comment || old.comment, userEmail, entryId, companyId]
);
await logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId);
return { entry: res.rows[0], wasNormalized };
}
async function deleteEntry(entryId, companyId, userEmail) {
const old = (await pool.query(
'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL',
[entryId, companyId]
)).rows[0];
if (!old) throw new Error('Запись не найдена');
await pool.query(
'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2',
[userEmail, entryId]
);
await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId);
}
// ── Export ──
async function getExportCIDRs() {
const rows = (await pool.query(
'SELECT value_cidr FROM whitelist_entries WHERE deleted_at IS NULL ORDER BY value_cidr'
)).rows;
return rows.map(r => r.value_cidr);
}
// ── Audit ──
async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId) {
await pool.query(
`INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id)
VALUES ($1, $2, $3, $4, $5, $6)`,
[userEmail, companyId, action, oldValue, newValue, entryId || null]
);
}
async function getAudit(companyId = null) {
let sql = 'SELECT * FROM audit_log';
const params = [];
if (companyId) {
sql += ' WHERE company_id = $1';
params.push(companyId);
}
sql += ' ORDER BY created_at DESC LIMIT 500';
return (await pool.query(sql, params)).rows;
}
module.exports = {
getOrCreateCompany, getLimit,
listEntries, createEntry, updateEntry, deleteEntry,
getExportCIDRs,
getAudit,
};
```
-59
View File
@@ -1,59 +0,0 @@
# Промпт для Claude Opus 4 — ревью схемы БД
## Контекст (не анализируй)
PostgreSQL. Микросервис IP WhiteList. Многоарендность: у каждой компании (companies) свои записи (whitelist_entries). Лимит по умолчанию 15 активных записей на компанию, custom_limit переопределяет. Soft delete. Аудит всех изменений (audit_log). Сервис на Node.js + pg pool.
## Что нужно
Ниже `schema.sql`. Найди:
- Отсутствующие индексы (полный скан таблиц под нагрузкой)
- Отсутствующие уникальные constraint'ы (дубликаты на уровне БД, не только в коде)
- Проблемы с внешними ключами (каскадное удаление?)
- Неоптимальные типы данных
- Уязвимости в структуре (можно ли обойти изоляцию через БД?)
- Что добавить для production
Ограничения:
- Только конкретные DDL-строки
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов
```sql
-- IP WhiteList schema
CREATE TABLE IF NOT EXISTS companies (
id SERIAL PRIMARY KEY,
client_id VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(255),
custom_limit INTEGER DEFAULT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS whitelist_entries (
id SERIAL PRIMARY KEY,
company_id INTEGER NOT NULL REFERENCES companies(id),
value_cidr VARCHAR(18) NOT NULL,
comment VARCHAR(255),
created_by VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by VARCHAR(255),
updated_at TIMESTAMPTZ,
deleted_by VARCHAR(255),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_entries_active
ON whitelist_entries(company_id, value_cidr) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS audit_log (
id SERIAL PRIMARY KEY,
user_email VARCHAR(255) NOT NULL,
company_id INTEGER NOT NULL,
action VARCHAR(32) NOT NULL,
old_value TEXT,
new_value TEXT,
entry_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_audit_company ON audit_log(company_id);
```
-124
View File
@@ -1,124 +0,0 @@
# Промпт для Claude Opus 4 — безопасность server.js (auth, CSRF, XSS)
## Контекст (не анализируй)
Node.js + Express + EJS + PostgreSQL. Микросервис IP WhiteList для облачного провайдера. Аутентификация через внешний auth-api (JWT в Authorization header). Сейчас DEV_MODE=true — временная заглушка. Продакшен с реальными токенами.
## Что нужно
Ниже полный код `server.js`. Найди:
- Баги в auth middleware (обход при DEV_MODE, пустой req.user)
- CSRF-уязвимости (POST-формы без токенов)
- XSS через query-параметры (error в редиректе)
- Race conditions на уровне роутов
- Проблемы обработки ошибок
- Небезопасные HTTP-заголовки
- Отсутствующие middleware (helmet, rate-limit, etc.)
- Несоответствия ТЗ (где обещано, но не реализовано)
Ограничения:
- Не предлагай менять стек
- Только конкретные строки и исправления
- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов. Чистый текст.
```js
const express = require('express');
const path = require('path');
require('dotenv').config();
const { checkConnection } = require('./src/db');
const q = require('./src/queries');
const app = express();
const PORT = process.env.PORT || 3000;
const DEV = process.env.DEV_MODE === 'true';
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true }));
// ── Auth middleware ──
app.use((req, res, next) => {
if (DEV) {
req.user = { email: 'dev@test.local', clientId: 'WZ01325', companyId: null, companyName: 'DEV' };
return next();
}
const auth = req.headers.authorization || '';
try {
const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString());
req.user = {
email: payload.email || 'unknown',
clientId: payload.ClientID,
companyId: payload.company_id,
companyName: payload.company_name || payload.ClientID,
};
} catch { req.user = {}; }
next();
});
// ── Health ──
app.get('/healthz', (req, res) => res.send('OK'));
// ── Главная ──
app.get('/', async (req, res) => {
const { clientId, companyName } = req.user;
try {
const company = await q.getOrCreateCompany(clientId, companyName);
const limit = await q.getLimit(company);
const entries = await q.listEntries(company.id);
res.render('index', { entries, limit, used: entries.length, user: req.user, error: null, message: null, wasNormalized: false });
} catch (e) {
res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: e.message, message: null, wasNormalized: false });
}
});
// ── Создать ──
app.post('/add', async (req, res) => {
const { value, comment } = req.body;
const { clientId, companyName, email } = req.user;
try {
const company = await q.getOrCreateCompany(clientId, companyName);
const result = await q.createEntry(company.id, value, comment, email);
const limit = await q.getLimit(company);
const entries = await q.listEntries(company.id);
res.render('index', {
entries, limit, used: entries.length, user: req.user,
message: result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено',
error: null, wasNormalized: result.wasNormalized,
});
} catch (e) {
const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null);
const entries = company ? await q.listEntries(company.id).catch(() => []) : [];
const limit = company ? await q.getLimit(company).catch(() => 15) : 15;
res.render('index', { entries, limit, used: entries.length, user: req.user, error: e.message, message: null, wasNormalized: false });
}
});
// ── Удалить (soft) ──
app.post('/delete/:id', async (req, res) => {
const { clientId, companyName, email } = req.user;
try {
const company = await q.getOrCreateCompany(clientId, companyName);
await q.deleteEntry(req.params.id, company.id, email);
res.redirect('/');
} catch (e) {
res.redirect('/?error=' + encodeURIComponent(e.message));
}
});
// ── Экспорт ──
app.get('/export', async (req, res) => {
try {
const cidrs = await q.getExportCIDRs();
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(cidrs.join('\n') + '\n');
} catch (e) {
res.status(500).send('Export error');
}
});
// ── Старт ──
checkConnection()
.then(() => console.log('DB connected'))
.catch(e => console.error('DB not ready:', e.message));
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
```
-107
View File
@@ -1,107 +0,0 @@
# Промпт для Claude Opus 4 — код-ревью validators.js
## Контекст (кратко, не анализируй — просто знай)
Делаем микросервис IP WhiteList для облачного провайдера. Клиенты управляют доверенными IPv4-адресами через веб-интерфейс. Стек: Node.js + Express + EJS + PostgreSQL.
Требования ТЗ к валидатору:
- Только IPv4, маска /22/32
- Нормализация host-битов (203.0.113.10/24 → 203.0.113.0/24), пользователь должен знать о нормализации
- Запрещены диапазоны: RFC1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10), Loopback (127/8), Link-local (169.254/16), IANA special (192.0.0/24), TEST-NET (192.0.2/24, 198.51.100/24, 203.0.113/24), Benchmarking (198.18/15), Multicast (224/4), Reserved (240/4), Limited broadcast (255.255.255.255/32)
- Проверка пересечений внутри компании, дубликатов, запрет вложенных подсетей
## Что нужно
Ниже код `validators.js`. Твоя задача — найти баги, уязвимости, несоответствия ТЗ и предложить исправления.
Ограничения:
- Не предлагай сменить язык/стек/фреймворк
- Не пиши «общие рекомендации» — только конкретные места с номерами строк
- Если предлагаешь исправить — напиши точный новый код
- Если багов нет — так и скажи
- **ВЕСЬ ОТВЕТ ДОЛЖЕН БЫТЬ В ОДНОМ БЛОКЕ — ОДИН markdown-блок, без интерактивных элементов, без свёрток, без ссылок на файлы. Чистый текст, готовый к копированию одной операцией.**
Файл:
```js
const net = require('net');
// Приложение А ТЗ — запрещённые диапазоны
const BLOCKED_RANGES = [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'192.0.0.0/24',
'192.0.2.0/24',
'198.51.100.0/24',
'203.0.113.0/24',
'198.18.0.0/15',
'224.0.0.0/4',
'240.0.0.0/4',
'255.255.255.255/32',
];
function validate(input) {
const raw = (input || '').trim();
if (!raw) throw new Error('Пустое значение');
if (raw.includes(':')) throw new Error('IPv6 не поддерживается');
if (/[a-zA-Z]/.test(raw.replace(/\./g, '').replace(/\//g, '').replace(/\d/g, '')))
throw new Error('Некорректный формат');
let cidr = raw.includes('/') ? raw : raw + '/32';
const [addr, maskStr] = cidr.split('/');
const mask = parseInt(maskStr, 10);
if (isNaN(mask) || mask < 22 || mask > 32) {
throw new Error('Маска должна быть от /22 до /32');
}
if (!net.isIPv4(addr)) throw new Error('Некорректный IPv4 адрес');
const ipNum = addr.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
const netMask = ~((1 << (32 - mask)) - 1) >>> 0;
const network = (ipNum & netMask) >>> 0;
const networkAddr = [
(network >>> 24) & 0xff,
(network >>> 16) & 0xff,
(network >>> 8) & 0xff,
network & 0xff,
].join('.');
const wasNormalized = addr !== networkAddr;
const normalized = networkAddr + '/' + mask;
for (const blocked of BLOCKED_RANGES) {
if (isSubnetOf(normalized, blocked)) {
throw new Error(`Диапазон ${normalized} запрещён (${blocked})`);
}
}
return { cidr: normalized, wasNormalized };
}
function overlaps(cidr1, cidr2) {
const a = cidrToRange(cidr1);
const b = cidrToRange(cidr2);
return a.start <= b.end && b.start <= a.start ||
b.start <= a.end && a.start <= b.start;
}
function isSubnetOf(cidr, parent) {
const child = cidrToRange(cidr);
const par = cidrToRange(parent);
return child.start >= par.start && child.end <= par.end;
}
function cidrToRange(cidr) {
const [addr, maskStr] = cidr.split('/');
const mask = parseInt(maskStr, 10);
const ip = addr.split('.').reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0;
const start = ip >>> 0;
const end = (ip | ((1 << (32 - mask)) - 1)) >>> 0;
return { start, end };
}
module.exports = { validate, overlaps, BLOCKED_RANGES };
```