Files
ipwhitelist-app/v2/history/2026-06-12.md
T

1005 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | Префиксы 1114 |
| 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 | Префиксы 1114 | 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.00.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" ✅
```