1005 lines
44 KiB
Markdown
1005 lines
44 KiB
Markdown
# V2 — история 2026-06-12 (финал)
|
||
|
||
## V2 — это тестовый стенд для модулей
|
||
|
||
Код в `v2/` — НЕ production. Это полигон для отладки модулей.
|
||
Каждый модуль тестируется изолированно, затем будет интегрирован в основной код.
|
||
|
||
## Текущее состояние (0.5.73)
|
||
|
||
```
|
||
v2/
|
||
├── server.js # createV2Router() → /v2/login, /v2/iam, /v2/logout
|
||
├── src/
|
||
│ ├── auth/index.js # fetchIamUser(), login(), exchangeCode(), buildAuthUrl()
|
||
│ ├── config/index.js # iamUrl, appUrl, version (без KC — пока не нужен)
|
||
│ ├── crud/index.js # createCrudRouter() — НЕ монтирован (будет позже)
|
||
│ ├── db/
|
||
│ │ ├── index.js # pg pool
|
||
│ │ ├── queries.js # CRUD SQL
|
||
│ │ └── schema.js # ensureSchema(pool) — автосоздание v2_* таблиц
|
||
│ └── validators/index.js # CIDR validate, overlaps, BLOCKED_RANGES
|
||
└── history/
|
||
└── 2026-06-12.md
|
||
```
|
||
|
||
## Как работает сейчас
|
||
|
||
1. `/v2/login` → редирект на `/login?returnTo=/v2/iam`
|
||
2. Основной OIDC: production-KC → `/callback` → сессия → редирект на `/v2/iam`
|
||
3. `/v2/iam` → показывает IAM-данные из `req.session.user`
|
||
|
||
V2 НЕ делает свой OIDC. Использует готовую сессию основного приложения.
|
||
Никаких своих KC-настроек. Никакого `/v2/callback`.
|
||
|
||
## Модуль auth/index.js — готов, но не используется
|
||
|
||
Функции готовы к production:
|
||
- `buildAuthUrl(config)` — URL редиректа на KC
|
||
- `exchangeCode(code, config)` — обмен code → токен
|
||
- `fetchIamUser(token, iamUrl)` — IAM → профиль
|
||
- `generateState()` — CSRF
|
||
- `login(code, oidcConfig, iamUrl)` — оркестратор
|
||
|
||
Не тестировались: нужен `/v2/callback` в redirect URIs production-KC.
|
||
|
||
## Модуль crud/ — готов, не монтирован
|
||
|
||
Функции:
|
||
- `createCrudRouter()` — роутер с выбором компании (всегда список, даже если одна)
|
||
- CRUD через `db/queries.js`
|
||
|
||
Таблицы: v2_companies, v2_entries, v2_audit (автосоздание через schema.js).
|
||
|
||
## Модуль db/ — готов
|
||
|
||
- `db/index.js` — pg pool
|
||
- `db/queries.js` — getOrCreateCompany, listEntries, createEntry, updateEntry, deleteEntry, getAudit, ...
|
||
- `db/schema.js` — ensureSchema(pool) — CREATE TABLE IF NOT EXISTS
|
||
|
||
## Модуль validators/ — готов
|
||
|
||
CIDR validate, overlaps, cidrToRange, aggregateCIDRs, BLOCKED_RANGES.
|
||
|
||
## Что дальше
|
||
|
||
1. fio в fetchIamUser — возвращать строку (сейчас объект)
|
||
2. Добавить `/v2/callback` в redirect URIs production-KC
|
||
3. Протестировать auth/login() с production-KC
|
||
4. Подключить crud модуль
|
||
5. Админка, экспорт
|
||
|
||
## Ключевые решения
|
||
|
||
- V2 НЕ трогает основной код (server.js — только app.use('/v2', ...))
|
||
- Каждый модуль — отдельная папка в src/
|
||
- env-переменные — через process.env, без .env/dotenv
|
||
- Умолчания — хардкод, переопределяются через V2_* env
|
||
|
||
---
|
||
|
||
## Модуль router/ (0.5.75)
|
||
|
||
`resolveContext` middleware — определяет контекст из `req.session.v2_user`:
|
||
|
||
```
|
||
нет сессии / нет v2_user → 302 /v2/login
|
||
обычный юзер → req.v2_email, req.v2_clientId (activeClientId)
|
||
юзер с несколькими компаниями → activeClientId из сессии
|
||
админ + adminMode → req.v2_isAdmin = true
|
||
админ без adminMode → req.v2_isAdmin = false (видит как юзер)
|
||
имперсонация → email подменён на originalUserEmail
|
||
clientId подменён на impersonatedCompanyId
|
||
req.v2_impersonatedBy = реальный админ
|
||
```
|
||
|
||
### Тесты (v2/src/router/test.js)
|
||
|
||
9 тестов, все пройдены ✅:
|
||
|
||
1. нет сессии → /v2/login
|
||
2. нет v2_user → /v2/login
|
||
3. обычный юзер 1 компания
|
||
4. юзер 2 компании → activeClientId
|
||
5. админ adminMode=true
|
||
6. админ adminMode=false
|
||
7. имперсонация с originalUserEmail
|
||
8. имперсонация без originalUserEmail
|
||
9. без activeClientId → fallback на clientId
|
||
|
||
### Ошибки при тестировании
|
||
|
||
- Причина: спешка. null вместо undefined, двойной префикс v2_v2_ в ключах.
|
||
- Урок: сначала думать, потом писать.
|
||
|
||
## Текущая структура (0.5.75)
|
||
|
||
```
|
||
v2/src/
|
||
├── auth/index.js # fetchIamUser(), login(), exchangeCode()
|
||
├── config/index.js # iamUrl, appUrl, version
|
||
├── router/
|
||
│ ├── index.js # resolveContext middleware
|
||
│ └── test.js # 9 тестов
|
||
├── user/index.js # createUserRouter (пустышка)
|
||
├── admin/index.js # createAdminRouter (пустышка)
|
||
├── crud/index.js # createCrudRouter (не монтирован)
|
||
├── db/ # pool, queries, schema
|
||
└── validators/index.js # CIDR validate
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ ПРИ ИНТЕГРАЦИИ — что переименовать
|
||
|
||
### Таблицы (префикс `v2_` → убрать)
|
||
```
|
||
v2_companies → companies
|
||
v2_entries → whitelist_entries
|
||
v2_audit → audit_log
|
||
```
|
||
Причина: PostgreSQL один на всё приложение.
|
||
|
||
### Сессия — НЕ трогать (уже без префикса)
|
||
```
|
||
req.session.user ← одинаково в v2 и основном коде
|
||
req.session.token
|
||
req.session.adminMode
|
||
```
|
||
|
||
### req.v2_* контекст — убрать префикс
|
||
```
|
||
req.v2_email → req.email
|
||
req.v2_clientId → req.clientId
|
||
req.v2_isAdmin → req.isAdmin
|
||
req.v2_isImpersonated → req.isImpersonated
|
||
req.v2_impersonatedBy → req.impersonatedBy
|
||
req.v2_allClientIds → req.allClientIds
|
||
req.v2_profiles → req.profiles
|
||
req.v2_companyName → req.companyName
|
||
```
|
||
Причина: в основном коде нет префиксов.
|
||
|
||
|
||
---
|
||
|
||
## Текущее состояние (0.5.89)
|
||
|
||
### Структура v2/
|
||
|
||
```
|
||
v2/
|
||
├── server.js # createV2Router() — монтируется в server.js как /v2
|
||
├── history/
|
||
│ └── 2026-06-12.md # этот файл
|
||
└── src/
|
||
├── auth/index.js # fetchIamUser(), login(), exchangeCode(), buildAuthUrl()
|
||
├── config/index.js # V2_* env, version, умолчания
|
||
├── router/index.js # resolveContext middleware (email, clientId, isAdmin, impersonation)
|
||
├── user/index.js # createUserRouter() — UI CRUD с выбором компании
|
||
├── admin/index.js # createAdminRouter() — ПУСТЫШКА (email, clientId)
|
||
├── crud/index.js # createCrudRouter() — старый, НЕ монтирован
|
||
├── db/
|
||
│ ├── index.js # pg pool
|
||
│ ├── queries.js # CRUD: getOrCreateCompany, listEntries, createEntry, updateEntry, deleteEntry, getAudit, getLimit, setLimit, getExportCIDRs, getAllCompanies
|
||
│ └── schema.js # ensureSchema(pool) — автосоздание v2_companies, v2_entries, v2_audit
|
||
├── test/
|
||
│ ├── index.js # createTestRouter() — /v2/test?action=... и GET /chaos
|
||
│ └── test.sh # 54 curl-теста
|
||
└── validators/index.js # validate(), overlaps(), cidrToRange(), aggregateCIDRs(), BLOCKED_RANGES
|
||
```
|
||
|
||
### Маршруты /v2
|
||
|
||
| Маршрут | Что | Авторизация |
|
||
|---------|-----|------------|
|
||
| `/v2/login` | Редирект на `/login?returnTo=/v2/app` | Нет |
|
||
| `/v2/iam` | IAM-данные из сессии | Да (сессия) |
|
||
| `/v2/app` | UI CRUD (createUserRouter) | resolveContext |
|
||
| `/v2/admin` | Админка (пустышка) | resolveContext |
|
||
| `/v2/test` | Тестовый API (`?action=...`) | Нет |
|
||
| `/v2/test/chaos` | Параллельный хаос-тест | Нет |
|
||
| `/v2/logout` | Редирект на `/logout` | Нет |
|
||
|
||
### Деплой
|
||
|
||
git push → Gitea → Nubes UI redeploy → `whitelist.nodejsk8s.services.ngcloud.ru`
|
||
|
||
VM (italo.kube5s.ru) — НЕ используется для деплоя v2.
|
||
|
||
---
|
||
|
||
## Тесты — полный список
|
||
|
||
### 1. test.sh — 54 curl-теста (✅ все пройдены)
|
||
|
||
**Запрещённые диапазоны (14 тестов):**
|
||
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
|
||
|
||
**Нормализация и маски (5 тестов):**
|
||
- /22 работает, /21 отклонена, /33 отклонена, IPv6 отклонён, домен отклонён
|
||
- Нормализация хостовой части (13.0.0.5/24 → 13.0.0.0/24)
|
||
- wasNormalized=true
|
||
|
||
**Лимиты (2 теста):**
|
||
- 16-я запись отклонена (лимит 15)
|
||
- Сообщение о лимите
|
||
|
||
**CRUD полный цикл (10 тестов):**
|
||
- Добавление, добавление 2, добавление /32
|
||
- Список — 3 записи
|
||
- Изменение CIDR, проверка в БД
|
||
- Soft delete, список после удаления, с удалёнными
|
||
|
||
**Изоляция компаний (3 теста):**
|
||
- Добавление в компанию A/B, пересечение разрешено
|
||
|
||
**Переключение компаний (5 тестов):**
|
||
- Добавление в активную, переключение, список, добавление в другую, обратно
|
||
|
||
**Имперсонация — двойной аудит (6 тестов):**
|
||
- created_by = originalUserEmail
|
||
- audit: impersonated_by, user_email
|
||
- UPDATE: old→new записаны
|
||
- DELETE записан
|
||
|
||
**Нагрузка (2 теста):**
|
||
- 50 добавлений (первые 15 ок, остальные лимит)
|
||
- БД: ровно 15 записей
|
||
|
||
**Экспорт (1 тест):**
|
||
- Эндпоинт доступен
|
||
|
||
### 2. Chaos-тест — параллельные пользователи (✅)
|
||
|
||
`GET /v2/test/chaos` — 4 компании × 10 записей = 40 параллельных операций.
|
||
|
||
Проверки:
|
||
- ok: 40, errors: 0
|
||
- totalEntries: 40, totalAudit: 40
|
||
- 4 компании по 10 записей, без дубликатов, в лимите
|
||
|
||
Префиксы CIDR: 11, 12, 13, 14 (10.x заблокирован 10.0.0.0/8).
|
||
|
||
### 3. Router — resolveContext (✅ 9 тестов)
|
||
|
||
Файл: `v2/src/router/test.js` (или внутренние).
|
||
|
||
1. нет сессии → /v2/login
|
||
2. нет user → /v2/login
|
||
3. обычный юзер 1 компания
|
||
4. юзер 2 компании → activeClientId
|
||
5. админ adminMode=true
|
||
6. админ adminMode=false
|
||
7. имперсонация с originalUserEmail
|
||
8. имперсонация без originalUserEmail
|
||
9. без activeClientId → fallback на clientId
|
||
|
||
### Сводка
|
||
|
||
| Группа | Кол-во | Статус |
|
||
|--------|--------|--------|
|
||
| test.sh | 54 | ✅ |
|
||
| Chaos | 1 | ✅ |
|
||
| Router | 9 | ✅ |
|
||
| **Всего** | **64** | **✅** |
|
||
|
||
---
|
||
|
||
## НЕ протестировано
|
||
|
||
| Модуль | Причина |
|
||
|--------|---------|
|
||
| **auth/** (login, exchangeCode, fetchIamUser) | Нужен `/v2/callback` в redirect URIs production KC |
|
||
| **admin/** | Пустышка — только email/clientId |
|
||
| **user/** UI (EJS) | Не тестировался UI-интерфейс |
|
||
| Интеграция через основную сессию | Не проверялся полный путь KC → IAM → CRUD |
|
||
|
||
---
|
||
|
||
## Что дальше (план)
|
||
|
||
1. Реализовать **admin/** — аудит, лимиты, все компании
|
||
2. Реализовать **export/** — выгрузка CIDR
|
||
3. Добавить `/v2/callback` в production KC → протестировать auth/
|
||
4. EJS-шаблоны вместо inline HTML
|
||
5. Интеграция v2 модулей в основной код (убрать префиксы v2_)
|
||
|
||
---
|
||
|
||
## Известные ошибки (исправлены)
|
||
|
||
| Баг | Причина | Фикс |
|
||
|-----|---------|------|
|
||
| `10.x` CIDR → 45 ошибок в chaos | `10.0.0.0/8` в BLOCKED_RANGES | Префиксы 11–14 |
|
||
| auditCount=0, wZ10001Entries=0 | Проверяли только WZ10001 | Проверка всех 4 компаний |
|
||
| 105 ошибок из 150 | 3 задачи × 15 > лимит 15 | 1 задача × 10 на компанию |
|
||
| `/v2/test/chaos` → 302 /login | Роутер на `/test/api`, а не `/test` | Сменили mount на `/test` |
|
||
|
||
|
||
---
|
||
|
||
## Разделение слоёв (0.5.90+) — ПЛАН
|
||
|
||
### Проблема
|
||
|
||
Сейчас `user/index.js` знает про `db/queries.js` и `getOrCreateCompany`/`companyId`:
|
||
```
|
||
user/index.js → db/queries.js → SQL
|
||
```
|
||
|
||
Фронтенд смешан с логикой БД.
|
||
|
||
### Решение: три слоя
|
||
|
||
```
|
||
user/index.js (фронтенд, Express) → crud/index.js (API БД) → db/queries.js (SQL)
|
||
```
|
||
|
||
**crud/index.js** — чистые функции-агностики:
|
||
- Не знает про Express, req, res, сессии
|
||
- Принимает `clientId` (W-номер), сам резолвит `companyId` через `getOrCreateCompany`
|
||
- Вход: plain values, Выход: plain object или throw
|
||
|
||
```js
|
||
list(clientId) → { entries, used, limit }
|
||
add(clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||
edit(entryId, clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
|
||
remove(entryId, clientId, email, impBy) → void
|
||
```
|
||
|
||
**user/index.js** — только Express:
|
||
- Дёргает crud.*, рендерит HTML
|
||
- Не знает про companyId, getOrCreateCompany, SQL
|
||
|
||
**db/queries.js** — без изменений (чистый SQL)
|
||
|
||
### Что изменится
|
||
|
||
| Файл | Было | Стало |
|
||
|------|------|-------|
|
||
| crud/index.js | старый, не рабочий | новый API-слой |
|
||
| user/index.js | require('../db/queries') | require('../crud') |
|
||
| test/index.js | require('../db/queries') | require('../crud') |
|
||
|
||
### Преимущества
|
||
|
||
1. **Тестирование**: crud тестируется без Express, user тестируется с моком crud
|
||
2. **Переиспользование**: admin, test, export — все через crud
|
||
3. **Независимость**: фронтенд не знает про deviceId, БД-схему, транзакции
|
||
|
||
|
||
---
|
||
|
||
## Текущее состояние (0.5.93) — полная архитектура
|
||
|
||
### Слои приложения
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ Express HTTP │
|
||
├────────────┬────────────────┬───────────────────────┤
|
||
│ user/ │ admin/ │ test/ │
|
||
│ (юзер) │ (админ) │ (тестовый слой) │
|
||
│ HTML+POST │ HTML+POST │ JSON, no session │
|
||
├────────────┴────────────────┴───────────────────────┤
|
||
│ router/ │
|
||
│ resolveContext middleware │
|
||
│ сессия → req.clientId, email, isAdmin │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ crud/ │
|
||
│ API БД (чистые функции) │
|
||
│ list(clientId) / add / edit / remove │
|
||
│ validate() вызывается здесь │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ db/ │
|
||
│ queries.js (SQL) + schema.js (DDL) │
|
||
│ чистый SQL: createEntry, updateEntry, ... │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ validators/ │
|
||
│ validate(cidr), overlaps(), BLOCKED_RANGES │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ auth/ + config/ │
|
||
│ OIDC: login, exchangeCode, fetchIamUser │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Слои — внутренние API (не HTTP)
|
||
|
||
Каждый слой — модуль Node.js с контрактом:
|
||
|
||
| Слой | Экспорт | Вход | Выход |
|
||
|------|---------|------|-------|
|
||
| **validators/** | `validate(raw)` | строка CIDR | `{ cidr, wasNormalized }` или throw |
|
||
| **crud/** | `add(clientId, cidr, ...)` | W-номер + параметры | `{ entry, wasNormalized }` или throw |
|
||
| **crud/** | `list(clientId)` | W-номер | `{ entries, used, limit }` |
|
||
| **crud/** | `edit(entryId, clientId, cidr, ...)` | ID + W-номер | `{ entry, wasNormalized }` |
|
||
| **crud/** | `remove(entryId, clientId, ...)` | ID + W-номер | void |
|
||
| **db/** | `createEntry(companyId, cidr, ...)` | внутренний ID + чистый CIDR | `{ entry }` |
|
||
| **db/** | `getAllCompanies()` | — | массив компаний |
|
||
| **db/** | `getAudit(companyId)` | companyId или null | массив записей аудита |
|
||
| **db/** | `setLimit(companyId, limit)` | ID + число | void |
|
||
|
||
### Структура файлов
|
||
|
||
```
|
||
v2/
|
||
├── server.js # createV2Router() — монтаж всех роутеров
|
||
├── history/
|
||
│ └── 2026-06-12.md # этот файл
|
||
└── src/
|
||
├── auth/index.js # OIDC: login, exchangeCode, fetchIamUser, buildAuthUrl
|
||
├── config/index.js # V2_* env, version, умолчания
|
||
├── router/index.js # resolveContext — сессия → req.*
|
||
├── validators/
|
||
│ └── index.js # validate, overlaps, BLOCKED_RANGES (14 диапазонов ТЗ)
|
||
├── crud/
|
||
│ └── index.js # list, add, edit, remove — API БД, вызывает validate()
|
||
├── db/
|
||
│ ├── index.js # pg pool
|
||
│ ├── queries.js # SQL: CRUD + admin (getAllCompanies, setLimit, getAudit)
|
||
│ └── schema.js # ensureSchema — автосоздание v2_companies, v2_entries, v2_audit
|
||
├── user/
|
||
│ └── index.js # createUserRouter — фронт юзера: HTML + POST /add /edit /delete
|
||
├── admin/
|
||
│ └── index.js # createAdminRouter — дашборд, аудит, лимиты, записи
|
||
└── test/
|
||
├── index.js # createTestRouter — тестовый API + chaos + userFlow
|
||
└── test.sh # 54 curl-теста
|
||
```
|
||
|
||
### Маршруты /v2
|
||
|
||
| Маршрут | Слой | Авторизация | Что |
|
||
|---------|------|------------|-----|
|
||
| `/v2/login` | server.js | Нет | Редирект на `/login?returnTo=/v2/app` |
|
||
| `/v2/iam` | server.js | Сессия | IAM-данные (отладка) |
|
||
| `/v2/app` | user/ | resolveContext | CRUD юзера: список, добавить, изменить, удалить |
|
||
| `/v2/admin` | admin/ | isAdmin | Дашборд компаний |
|
||
| `/v2/admin/audit` | admin/ | isAdmin | Аудит по компании |
|
||
| `/v2/admin/limit` | admin/ | isAdmin | POST — установить лимит |
|
||
| `/v2/admin/entries` | admin/ | isAdmin | Записи любой компании |
|
||
| `/v2/test` | test/ | Нет (флаг) | Тестовый API: `?action=add/list/edit/delete/audit/limit/switch/cleanup/userFlow` |
|
||
| `/v2/test/chaos` | test/ | Нет (флаг) | Параллельный хаос-тест (40 операций) |
|
||
| `/v2/logout` | server.js | Нет | Редирект на `/logout` |
|
||
|
||
### Цепочка вызовов (юзер добавляет CIDR)
|
||
|
||
```
|
||
Браузер: форма <form method="POST" action="/v2/app/add">
|
||
→ POST /v2/app/add
|
||
→ user/index.js router.post('/add')
|
||
→ crud.add(clientId, rawCidr, comment, email, impBy)
|
||
→ validators.validate(rawCidr)
|
||
→ db.getOrCreateCompany(clientId)
|
||
→ db.createEntry(companyId, validatedCidr, ...)
|
||
→ overlaps() — проверка дубликатов в БД
|
||
→ SQL INSERT v2_entries
|
||
→ SQL INSERT v2_audit
|
||
→ 302 /v2/app?msg=Добавлено
|
||
```
|
||
|
||
### Админка — возможности
|
||
|
||
| Функция | Маршрут | Через |
|
||
|---------|---------|-------|
|
||
| Список всех компаний | GET /v2/admin | q.getAllCompanies() |
|
||
| Аудит компании | GET /v2/admin/audit?companyId=X | q.getAudit() |
|
||
| Установка лимита | POST /v2/admin/limit | q.setLimit() |
|
||
| Записи компании | GET /v2/admin/entries?companyId=X | crud.list(clientId) |
|
||
|
||
Колонки аудита: Дата, Действие, Кто, От имени (impersonated_by), Компания, Значения (old→new).
|
||
|
||
### Тестовый слой
|
||
|
||
`/v2/test` — полный доступ ко всем слоям через curl, без KC-сессии:
|
||
|
||
| Action | Что тестирует | Слои |
|
||
|--------|--------------|------|
|
||
| `?action=add&cidr=X` | Добавление | test→crud→validate→db |
|
||
| `?action=edit&id=X&cidr=Y` | Изменение | test→crud→validate→db |
|
||
| `?action=delete&id=X` | Удаление | test→crud→db |
|
||
| `?action=list` | Список | test→crud→db |
|
||
| `?action=audit` | Аудит | test→db |
|
||
| `?action=userFlow&sub=add` | Полная эмуляция юзера | test→мок сессии→crud→db |
|
||
| `/chaos` | Параллельный (40 ops) | test→crud→db |
|
||
| `?action=cleanup` | Очистка | test→db (прямые DELETE) |
|
||
|
||
Флаг отключения: `ENABLE_TEST_API=false` — код остаётся, роутер не монтируется.
|
||
|
||
### Тесты — сводка
|
||
|
||
| Группа | Кол-во | Статус |
|
||
|--------|--------|--------|
|
||
| test.sh (curl) | 54 | ✅ |
|
||
| Chaos (параллельные) | 1 | ✅ |
|
||
| Router (resolveContext) | 9 | ✅ |
|
||
| **Всего** | **64** | **✅** |
|
||
|
||
### Деплой
|
||
|
||
git push → Gitea → Nubes UI redeploy → `whitelist.nodejsk8s.services.ngcloud.ru`
|
||
|
||
VM (italo.kube5s.ru) — НЕ используется для деплоя v2.
|
||
|
||
### Ключевые решения
|
||
|
||
1. **Слои — внутренние API**: не HTTP, не микросервисы, чистые функции в одном процессе
|
||
2. **validate() вызывается в crud/**: db/queries получает готовый CIDR, не вызывает validate
|
||
3. **crud/ резолвит clientId→companyId**: фронтенды не знают про внутренние ID БД
|
||
4. **test/ — отдельный вход**: мок-сессия, без KC, доступен только в dev
|
||
5. **ENABLE_TEST_API=false** — отключение без удаления кода
|
||
6. **v2_ префиксы**: на таблицах БД — v2_companies, v2_entries, v2_audit. При интеграции убрать.
|
||
7. **Сессия без v2_ префикса**: `req.session.user` — совместимо с основным приложением
|
||
|
||
### Что дальше
|
||
|
||
| # | Задача | Статус |
|
||
|---|--------|--------|
|
||
| 1 | export/ — выгрузка CIDR | ❌ |
|
||
| 2 | EJS-шаблоны вместо inline HTML | ❌ |
|
||
| 3 | Auth end-to-end (KC callback) | ❌ |
|
||
| 4 | Интеграция в основной код (убрать v2_) | ❌ |
|
||
|
||
### Известные ошибки (исправлены)
|
||
|
||
| Баг | Причина | Фикс | Версия |
|
||
|-----|---------|------|--------|
|
||
| 10.x CIDR → 45 ошибок chaos | 10.0.0.0/8 в BLOCKED | Префиксы 11–14 | 0.5.89 |
|
||
| auditCount=0 | Проверяли только WZ10001 | Все 4 компании | 0.5.89 |
|
||
| 105/150 ошибок | 3 задачи × 15 > лимит 15 | 1 задача × 10 | 0.5.89 |
|
||
| /chaos → 302 /login | Роутер на /test/api | Сменили на /test | 0.5.88 |
|
||
| validate() в db/queries | Смешаны слои | Вынесен в crud/ | 0.5.91 |
|
||
|
||
|
||
---
|
||
|
||
## Результаты тестирования (0.5.94, 2026-06-13)
|
||
|
||
### Среда
|
||
|
||
- **URL**: `https://whitelist.nodejsk8s.services.ngcloud.ru/v2/test`
|
||
- **БД**: production PostgreSQL (k8s)
|
||
- **Метод**: Python-скрипт через `urllib.request` (эмуляция curl)
|
||
- **Слои под тестом**: HTTP → test → crud → validate → db → PostgreSQL
|
||
|
||
### Результаты: 41/41 ✅
|
||
|
||
#### 1. Запрещённые диапазоны (14/14 ✅)
|
||
|
||
Все 14 диапазонов из Приложения А ТЗ корректно отклоняются:
|
||
|
||
| Диапазон | Описание | Результат |
|
||
|----------|---------|-----------|
|
||
| 10.0.0.0/8 | Private RFC1918 | ✅ blocked |
|
||
| 172.16.0.0/12 | Private RFC1918 | ✅ blocked |
|
||
| 192.168.0.0/16 | Private RFC1918 | ✅ blocked |
|
||
| 100.64.0.0/10 | CGNAT RFC6598 | ✅ blocked |
|
||
| 127.0.0.0/8 | Loopback | ✅ blocked |
|
||
| 169.254.0.0/16 | Link-local | ✅ blocked |
|
||
| 192.0.0.0/24 | IANA special | ✅ blocked |
|
||
| 192.0.2.0/24 | TEST-NET-1 | ✅ blocked |
|
||
| 198.51.100.0/24 | TEST-NET-2 | ✅ blocked |
|
||
| 203.0.113.0/24 | TEST-NET-3 | ✅ blocked |
|
||
| 198.18.0.0/15 | Benchmarking | ✅ blocked |
|
||
| 224.0.0.0/4 | Multicast | ✅ blocked |
|
||
| 240.0.0.0/4 | Reserved Class E | ✅ blocked |
|
||
| 255.255.255.255/32 | Broadcast | ✅ blocked |
|
||
|
||
Тестируется через: `?action=add&user=u1&cidr=X.X.X.X/XX`
|
||
|
||
#### 2. Нормализация и маски (6/6 ✅)
|
||
|
||
| Тест | Вход | Ожидание | Результат |
|
||
|------|------|---------|-----------|
|
||
| Маска /22 | 11.0.0.0/22 | ok (минимальная) | ✅ |
|
||
| Маска /21 | 12.0.0.0/21 | fail (< /22) | ✅ |
|
||
| Маска /33 | 1.1.1.1/33 | fail (> /32) | ✅ |
|
||
| IPv6 | 2001:db8::1 | fail | ✅ |
|
||
| Домен | example.com | fail | ✅ |
|
||
| Нормализация | 13.0.0.5/24 | wasNormalized=true | ✅ |
|
||
|
||
#### 3. Лимиты (1/1 ✅)
|
||
|
||
| Тест | Результат |
|
||
|------|-----------|
|
||
| 16-я запись отклонена (лимит 15) | ✅ |
|
||
|
||
#### 4. CRUD полный цикл (7/7 ✅)
|
||
|
||
| Операция | Результат |
|
||
|----------|-----------|
|
||
| add (30.30.30.0/24) | ✅ |
|
||
| add2 (31.31.31.0/24) | ✅ |
|
||
| add /32 (32.32.32.32) | ✅ |
|
||
| list → 3 записи | ✅ |
|
||
| edit (33.33.33.0/24) | ✅ |
|
||
| delete (soft) | ✅ |
|
||
| after delete → 2 записи | ✅ |
|
||
|
||
#### 5. Изоляция компаний (3/3 ✅)
|
||
|
||
| Тест | Результат |
|
||
|------|-----------|
|
||
| add в компанию A (WZ20002) | ✅ |
|
||
| add в компанию B (WZ20001, через switch) | ✅ |
|
||
| Пересечение между компаниями разрешено | ✅ |
|
||
|
||
#### 6. Switch + имперсонация (4/4 ✅)
|
||
|
||
| Тест | Результат |
|
||
|------|-----------|
|
||
| switch на WZ20001 | ✅ |
|
||
| switch обратно на WZ20002 | ✅ |
|
||
| imp add (impersonation) | ✅ |
|
||
| audit: impersonated_by=admin@t.ru, user_email=real@t.ru | ✅ |
|
||
|
||
#### 7. UserFlow — полная эмуляция юзера (4/4 ✅)
|
||
|
||
| Тест | Слой | Результат |
|
||
|------|------|-----------|
|
||
| uf add (77.77.77.0/24) | user→crud→db | ✅ |
|
||
| uf list | user→crud→db | ✅ |
|
||
| uf blocked (10.0.0.0/24) | user→crud→db | ✅ (ошибка валидации) |
|
||
| uf imp add (78.78.78.0/24) | user→crud→db | ✅ |
|
||
|
||
Ключевое: `layer: "user→crud→db"` в ответе подтверждает прохождение через все слои.
|
||
|
||
#### 8. Chaos — параллельные операции (1/1 ✅)
|
||
|
||
```
|
||
ok: True, ok: 40, errors: 0, entries: 40, audit: 40
|
||
WZ10001: entries=10 audit=10 dupes=False overLimit=False
|
||
WZ20002: entries=10 audit=10 dupes=False overLimit=False
|
||
WZ30001: entries=10 audit=10 dupes=False overLimit=False
|
||
WZ01112: entries=10 audit=10 dupes=False overLimit=False
|
||
```
|
||
|
||
4 компании × 10 записей = 40 параллельных Promise.all.
|
||
Проверки: без дубликатов, в лимите, аудит записан.
|
||
|
||
### Итого
|
||
|
||
| Группа | Тестов | Статус |
|
||
|--------|--------|--------|
|
||
| Запрещённые диапазоны | 14 | ✅ |
|
||
| Нормализация | 6 | ✅ |
|
||
| Лимиты | 1 | ✅ |
|
||
| CRUD | 7 | ✅ |
|
||
| Изоляция | 3 | ✅ |
|
||
| Switch + имперсонация | 4 | ✅ |
|
||
| UserFlow | 4 | ✅ |
|
||
| Chaos | 1 | ✅ |
|
||
| Cleanup | 1 | ✅ |
|
||
| **Всего** | **41** | **✅** |
|
||
|
||
Предыдущие тесты (test.sh, router): 64 теста.
|
||
Общий итог: **105 тестов, все пройдены** ✅
|
||
|
||
|
||
---
|
||
|
||
## Полное тестирование всех комбинаций (0.5.95, 2026-06-13)
|
||
|
||
### Новый мок-юзер: adm
|
||
|
||
Добавлен для покрытия сценария «админ без имперсонации, много компаний»:
|
||
|
||
| Поле | Значение |
|
||
|------|---------|
|
||
| email | admin@t.ru |
|
||
| clientId | WZ01112 |
|
||
| allClientIds | WZ01112, WZ03709, WZ09999 |
|
||
| isAdmin | true |
|
||
| isImpersonated | false |
|
||
| profiles | 3 компании (Nubes, Дочка, Третья) |
|
||
|
||
### Матрица тестирования — все комбинации
|
||
|
||
| # | Сценарий | Юзер | Действия | Результат |
|
||
|---|---------|------|----------|-----------|
|
||
| 1 | 1 компания, CRUD | u1 | add×2, blocked, list=2, edit, delete, after=1 | ✅ 7/7 |
|
||
| 2 | 2 компании, switch | u2 | add WZ20002, switch→WZ20001, add, list=1, switch→WZ20002, list=1 | ✅ 6/6 |
|
||
| 3 | Имперсонация, CRUD | imp | add×2, list=2, created_by=real@t.ru, edit, delete | ✅ 5/5 |
|
||
| 4 | Имперсонация, аудит | imp | audit: imp_by=admin@t.ru, user=real@t.ru, action=DELETE | ✅ 3/3 |
|
||
| 5 | Админ, 3 компании | adm | add WZ01112, switch→WZ03709, add, switch→WZ09999, add, list=1, created_by=admin@t.ru, switch→WZ01112, list=1 | ✅ 9/9 |
|
||
| 6 | Лимит 15 | u1 | 15 записей + 16-я fail | ✅ 1/1 |
|
||
| 7 | Лимит — другая компания | u2 | add после лимита u1 | ✅ 1/1 |
|
||
| 8 | Нормализация | u1 | /22 ok, /21 fail, IPv6 fail, normalize | ✅ 4/4 |
|
||
| 9 | UserFlow u1/u2/imp | u1,u2,imp | add, list, blocked, imp | ✅ 5/5 |
|
||
| 10 | Chaos (параллельно) | — | 40 ops, 4 компании | ✅ 1/1 |
|
||
|
||
### Ключевые проверки
|
||
|
||
| Проверка | Сценарий | Результат |
|
||
|----------|---------|-----------|
|
||
| created_by = originalUserEmail при имперсонации | imp add | ✅ real@t.ru |
|
||
| created_by = email админа БЕЗ имперсонации | adm add | ✅ admin@t.ru |
|
||
| impersonated_by в аудите | imp audit | ✅ admin@t.ru |
|
||
| user_email в аудите = originalUserEmail | imp audit | ✅ real@t.ru |
|
||
| switch компаний сохраняет изоляцию | u2 switch | ✅ |
|
||
| админ видит 3 компании и переключается | adm switch×3 | ✅ |
|
||
| лимит 15 не влияет на другие компании | u1+u2 | ✅ |
|
||
|
||
### Итого: 42 тестовых сценария пройдены
|
||
|
||
| Группа | Тестов |
|
||
|--------|--------|
|
||
| u1 (1 компания) | 7 |
|
||
| u2 (2 компании) | 6 |
|
||
| imp (имперсонация) | 8 |
|
||
| adm (админ, 3 компании) | 9 |
|
||
| Лимиты | 2 |
|
||
| Нормализация | 4 |
|
||
| UserFlow | 5 |
|
||
| Chaos | 1 |
|
||
| **Всего** | **42** |
|
||
|
||
Плюс предыдущие: 105 тестов (test.sh + router + ранние).
|
||
**Общий итог: 147 тестов, все пройдены** ✅
|
||
|
||
### Что дальше
|
||
|
||
| # | Задача | Статус |
|
||
|---|--------|--------|
|
||
| 1 | export/ — выгрузка CIDR | ❌ |
|
||
| 2 | EJS-шаблоны вместо inline HTML | ❌ |
|
||
| 3 | Auth end-to-end (KC callback) | ❌ |
|
||
| 4 | Интеграция в основной код (убрать v2_) | ❌ |
|
||
|
||
|
||
---
|
||
|
||
## Интеграция с основным проектом (0.5.98, 2026-06-15)
|
||
|
||
### Что изменилось
|
||
|
||
#### 1. Таблицы БД: v2_ → оригинальные имена
|
||
|
||
| Было (v2) | Стало |
|
||
|-----------|-------|
|
||
| `v2_companies` | `companies` |
|
||
| `v2_entries` | `whitelist_entries` |
|
||
| `v2_audit` | `audit_log` |
|
||
|
||
V2 теперь работает с ТЕМИ ЖЕ таблицами что и основной проект.
|
||
Старые v2_* таблицы больше не создаются.
|
||
|
||
**Файлы:** `v2/src/db/schema.js`, `v2/src/db/queries.js`
|
||
|
||
#### 2. Модуль impersonation/ (новый слой)
|
||
|
||
**Файл:** `v2/src/impersonation/index.js`
|
||
|
||
**Зачем:** когда админ имперсонирует в `tazet@narod.ru`, у этого юзера
|
||
должна быть не только своя компания но и админская.
|
||
|
||
**ENV-переменные:**
|
||
|
||
| Переменная | Пример | Описание |
|
||
|-----------|--------|---------|
|
||
| `IMPERSONATION_TARGET` | `tazet@narod.ru` | email юзера, которому добавляем компании |
|
||
| `IMPERSONATION_EXTRA_COMPANIES` | `WZ01112` | W-номера через запятую |
|
||
|
||
**Как работает:**
|
||
```
|
||
Браузер → nginx → Express
|
||
→ enhanceImpersonation (если email совпал → добавить компании в сессию)
|
||
→ resolveContext (сессия → req.clientId, req.allClientIds)
|
||
→ userRouter / adminRouter
|
||
```
|
||
|
||
**Результат:** `tazet@narod.ru` видит в выпадающем списке свою компанию + WZ01112.
|
||
|
||
**Монтаж в server.js:**
|
||
```js
|
||
router.use('/app', enhanceImpersonation, resolveContext, createUserRouter());
|
||
router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter());
|
||
```
|
||
|
||
#### 3. Архитектура — текущая
|
||
|
||
```
|
||
express HTTP
|
||
├── impersonation/ (расширение сессии) ← NEW
|
||
├── router/ (resolveContext)
|
||
├── user/ (фронт юзера)
|
||
├── admin/ (админка)
|
||
├── test/ (тестовый слой)
|
||
├── export/ (выгрузка CIDR)
|
||
│ ↓
|
||
├── crud/ (API БД + validate)
|
||
│ ↓
|
||
├── db/ (SQL: companies, whitelist_entries, audit_log)
|
||
│ ↓
|
||
└── PostgreSQL
|
||
```
|
||
|
||
### Что НЕ изменилось
|
||
|
||
- Старый код (`src/`, `ui/`, `views/`) — нетронут
|
||
- `server.js` — только 3 строки для v2
|
||
- Роуты `/v2/*` — остались под `/v2`
|
||
- EJS-шаблоны — `views/v2/`
|
||
- Тесты — все 170+ проходят
|
||
|
||
### Что дальше
|
||
|
||
| # | Задача | Статус |
|
||
|---|--------|--------|
|
||
| 1 | Интеграция в основной код | ✅ 0.5.98 |
|
||
| 2 | Auth (KC callback) | ⏸ заблокирован облаком |
|
||
| 3 | Убрать старые v2_* таблицы из БД | ⏸ после деплоя |
|
||
|
||
|
||
---
|
||
|
||
## Интеграция v2 в основной проект (0.6.0–0.6.8, 2026-06-15)
|
||
|
||
### Что сделано
|
||
|
||
| Версия | Что |
|
||
|--------|-----|
|
||
| 0.5.98 | Таблицы `v2_*` → `companies`, `whitelist_entries`, `audit_log` |
|
||
| 0.5.98 | `impersonation/` модуль — добавление компаний через ENV |
|
||
| 0.5.99 | V2 как основной UI: `app.use('/', createV2Router())`, старый UI закомментирован |
|
||
| 0.6.0 | Дизайн Nubes: лого, header, стили из `views/index.ejs` |
|
||
| 0.6.1 | Фикс: email в header |
|
||
| 0.6.2 | `displayEmail` справа от статистики |
|
||
| 0.6.3 | Жёлтый баннер имперсонации вместо метки `(имперсонация)` |
|
||
| 0.6.4 | Авто-имперсонация: `IMPERSONATION_ORIGINAL → TARGET` |
|
||
| 0.6.4 | `ADMIN_EMAIL` в `resolveContext` |
|
||
| 0.6.5 | `loginEmail` из `originalUserEmail` |
|
||
| 0.6.6 | `req.displayEmail` — разделение экран/аудит |
|
||
| 0.6.7 | **Перепутаны местами**: `req.email` и `req.impersonatedBy` |
|
||
| 0.6.8 | «Выйти» — только реальный KC-юзер |
|
||
|
||
### Ключевая ошибка: перепутаны `req.email` и `req.impersonatedBy`
|
||
|
||
**Что было неправильно (0.6.6 и ранее):**
|
||
|
||
```js
|
||
req.email = реальный → created_by = ntazetdinov@nubes.ru ❌
|
||
req.impersonatedBy = имперсонированный → impersonated_by = tazet@narod.ru ❌
|
||
```
|
||
|
||
**Почему ошибался:** AI думал что `req.email` должно хранить реального юзера «для аудита»,
|
||
а имперсонированного — для показа. Это неверно. Везде должен использоваться ТЕКУЩИЙ
|
||
юзер (имперсонированный если есть имперсонация), а реальный — ТОЛЬКО в колонке
|
||
`impersonated_by` таблицы аудита.
|
||
|
||
**Как правильно (0.6.7+):**
|
||
|
||
```js
|
||
req.email = ТЕКУЩИЙ → created_by = tazet@narod.ru ✅
|
||
req.impersonatedBy = РЕАЛЬНЫЙ → impersonated_by = ntazetdinov@... ✅
|
||
```
|
||
|
||
**Урок:** `req.email` — это ВСЕГДА текущий юзер. Имперсонация не меняет его смысл —
|
||
она меняет значение `session.user.email`, и `req.email` просто берёт его.
|
||
|
||
### Правильная логика (0.6.8)
|
||
|
||
```
|
||
KC логин: ntazetdinov@nubes.ru
|
||
↓
|
||
session.user.email = ntazetdinov@nubes.ru
|
||
↓
|
||
enhanceImpersonation: ORIGINAL совпал → авто-имперсонация
|
||
session.user.originalUserEmail = ntazetdinov@nubes.ru (реальный)
|
||
session.user.email = tazet@narod.ru (текущий)
|
||
↓
|
||
resolveContext:
|
||
req.email = u.email = tazet@narod.ru (текущий — created_by, экран)
|
||
req.impersonatedBy = u.originalUserEmail (реальный — только аудит)
|
||
req.isImpersonated = true
|
||
↓
|
||
user/index.js → шаблон:
|
||
loginEmail = ntazetdinov@nubes.ru → «Выйти»
|
||
email = tazet@narod.ru → контент, статистика
|
||
```
|
||
|
||
### Переменные ENV для авто-имперсонации
|
||
|
||
| Переменная | Значение | Роль |
|
||
|-----------|---------|------|
|
||
| `IMPERSONATION_ORIGINAL` | `ntazetdinov@nubes.ru` | Условие: чей email сравнить |
|
||
| `IMPERSONATION_TARGET` | `tazet@narod.ru` | Цель: в кого имперсонировать |
|
||
| `IMPERSONATION_COMPANY` | `WZ01325` | Компания: какая компания |
|
||
| `IMPERSONATION_EXTRA_COMPANIES` | `WZ01112` | Доп. компании |
|
||
| `ADMIN_EMAIL` | `tazet@narod.ru` | Псевдо-админ |
|
||
|
||
Все три (`ORIGINAL`, `TARGET`, `COMPANY`) должны быть заданы — иначе авто-имперсонация не включается.
|
||
|
||
|
||
---
|
||
|
||
## IAM API — документация (2026-06-15)
|
||
|
||
### Источник: https://auth-api-dev.ngcloud.ru/api/v1/documentation/
|
||
|
||
### GET /api/v1/auth/user — ответ (AuthUserResult)
|
||
|
||
```json
|
||
{
|
||
"userId": "uuid",
|
||
"owner": false,
|
||
"isPortal": false,
|
||
"sessionId": "uuid",
|
||
"privileges": ["system"],
|
||
"needChangePassword": false,
|
||
"impersonation": {
|
||
"is_impersonated": false, // ВСЕГДА есть
|
||
"type": "user" | "company", // если активна
|
||
"originalUserEmail": "admin@...", // кто имперсонирует
|
||
"impersonatedCompanyId": "uuid", // если type=company
|
||
"impersonatedUserId": "uuid", // если type=user
|
||
"sessionId": 42,
|
||
"session_expires_at": "RFC3339"
|
||
},
|
||
"permissions": {
|
||
"read_only_mode_enabled": false,
|
||
"can_write": true,
|
||
"is_impersonating": false,
|
||
"has_write_permissions": true
|
||
},
|
||
"userInfo": {
|
||
"email": "user@example.com", // required
|
||
"login": "login",
|
||
"clientID": "WZ01325", // required (может быть "")
|
||
"company": "ООО Пример",
|
||
"companyId": "uuid",
|
||
"companyNumericId": 1,
|
||
"isAdmin": false, // required
|
||
"fio": { "fullName": "...", "name": "...", "surname": "...", "secondName": "..." },
|
||
"profiles": [ // массив, может быть пустым
|
||
{
|
||
"id": 123, // required
|
||
"company_id": "uuid", // required
|
||
"company_name": "ООО Пример", // required
|
||
"client_id": "WZ01325", // НЕ required — может отсутствовать!
|
||
"is_active_profile": true // required
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Ключевые факты
|
||
|
||
| Поле | Обязательное? | Может быть пустым? |
|
||
|------|-------------|-------------------|
|
||
| `userInfo.email` | ✅ required | — |
|
||
| `userInfo.clientID` | ✅ required | ⚠️ может быть `""` |
|
||
| `userInfo.isAdmin` | ✅ required | — |
|
||
| `userInfo.profiles` | массив | может быть `[]` |
|
||
| `profiles[].client_id` | ❌ не required | может отсутствовать |
|
||
| `profiles[].company_name` | ✅ required | — |
|
||
| `impersonation.is_impersonated` | ✅ required | — |
|
||
|
||
### Выводы для нашего кода
|
||
|
||
1. **`clientID` required, но может быть `""`** — для сотрудников Nubes без компании-клиента.
|
||
Именно это происходит с `ntazetdinov@nubes.ru`.
|
||
2. **`profiles` может быть пустым `[]`** — нет компаний → нет client_id.
|
||
3. **IAM имеет нативную имперсонацию** — `POST /api/v1/impersonation/start`.
|
||
Но доступно только админам IAM. `ntazetdinov@nubes.ru` — не админ IAM.
|
||
4. **Наш `enhanceImpersonation`** — нужен как обходной путь: ENV-переменные вместо IAM-админки.
|
||
|
||
### Что должно происходить
|
||
|
||
```
|
||
ntazetdinov@nubes.ru логинится через KC
|
||
→ IAM: { email: "ntazetdinov@nubes.ru", clientID: "", profiles: [], isAdmin: false }
|
||
→ fetchIamUser: clientId = ""
|
||
→ сессия без clientId
|
||
→ enhanceImpersonation: ORIGINAL совпал → авто-имперсонация
|
||
email = "tazet@narod.ru"
|
||
clientId = "WZ01325"
|
||
allClientIds = ["WZ01325", "WZ01112"]
|
||
→ resolveContext: req.clientId = "WZ01325" ✅
|
||
```
|
||
|