From 32671bfa43a800d5e205146874d6285b2d97808a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 19:12:19 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D1=88=D0=B8=D0=B5=D1=81?= =?UTF-8?q?=D1=8F=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20(=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B2=20ipwhiteli?= =?UTF-8?q?st-app)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/[DEPRECATED]-architecture.md | 169 ++++++++++++++++++++++++++ docs/[DEPRECATED]-plan-sonnet.md | 96 +++++++++++++++ docs/[DEPRECATED]-questions-sonnet.md | 70 +++++++++++ 3 files changed, 335 insertions(+) create mode 100644 docs/[DEPRECATED]-architecture.md create mode 100644 docs/[DEPRECATED]-plan-sonnet.md create mode 100644 docs/[DEPRECATED]-questions-sonnet.md diff --git a/docs/[DEPRECATED]-architecture.md b/docs/[DEPRECATED]-architecture.md new file mode 100644 index 0000000..7efc93f --- /dev/null +++ b/docs/[DEPRECATED]-architecture.md @@ -0,0 +1,169 @@ +# 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 diff --git a/docs/[DEPRECATED]-plan-sonnet.md b/docs/[DEPRECATED]-plan-sonnet.md new file mode 100644 index 0000000..21da68d --- /dev/null +++ b/docs/[DEPRECATED]-plan-sonnet.md @@ -0,0 +1,96 @@ +# 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` с пресетами + кастомные поля | diff --git a/docs/[DEPRECATED]-questions-sonnet.md b/docs/[DEPRECATED]-questions-sonnet.md new file mode 100644 index 0000000..e612889 --- /dev/null +++ b/docs/[DEPRECATED]-questions-sonnet.md @@ -0,0 +1,70 @@ +# Вопросы к 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): временно, заменить после ответа команды +``` +