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

33 KiB
Raw Blame History

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
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_)