v0.5.2: multi-company, isAdmin fix, appVersion, docs

This commit is contained in:
2026-06-02 10:52:12 +03:00
parent 07528b0de3
commit a083f65323
13 changed files with 616 additions and 61 deletions
+205
View File
@@ -0,0 +1,205 @@
# Keycloak Auth Reference — консолидированная справка
> Обновлено: 2026-06-02
> Файл-ориентир. Править по мере уточнения у DevOps и продвижения проекта.
---
## 1. Keycloak-эндпоинты
Realm: **`cloud`** на `keycloak.nubes.ru`
| Назначение | URL |
|---|---|
| Authorization | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth` |
| Token exchange | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token` |
| JWKS (сертификаты) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs` |
| Logout | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout` |
---
## 2. Зарегистрированное приложение (OAuth client)
| Параметр | Значение |
|---|---|
| `client_id` | `deck-test.ngcloud.ru` |
Источник: HAR-файл `Files/nubes_login.har` (в query-параметрах auth request).
---
## 3. Схема аутентификации: Authorization Code Flow
```
1. Браузер → /login (приложение)
2. Приложение → 302 → Keycloak /auth?client_id=...&scope=openid&redirect_uri=...&state=...
3. Пользователь вводит логин/пароль в Keycloak
4. Пользователь вводит OTP (MFA)
5. Keycloak → 302 → /callback?code=...&state=...
6. Приложение → POST /token (code → access_token + refresh_token + id_token)
7. Валидация JWT через JWKS (RS256)
8. Извлечение claims → req.user → сессия
```
---
## 4. Получение токена через API Gateway (альтернативный путь)
Из HAR: код обменивается не напрямую в Keycloak, а через **API Gateway ngcloud**:
```
POST https://lk-api-gateway-test.ngcloud.ru/api/v1/iam/auth/token
Content-Type: application/json
Body: {"code": "<authorization_code>"}
```
Ответ:
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 43200
}
```
---
## 5. JWT-токены
### 5.1. Токен из `Files/token.txt` (test-окружение)
| Поле | Значение |
|---|---|
| `iss` | `https://auth.k8s.ngcloud.ru` |
| `aud` | `backend` |
| `sub` | `tazetdinovn@gmail.com` |
| `name` | Наиль Тазетдинов |
| `email` | `tazetdinovn@gmail.com` |
| `expires_in` | 86400 (24ч) |
| `scope` | `openid` |
| `token_type` | `Bearer` |
Claims (массив):
- `""`
- `""`
- `wz03709-shturval-admin-iot-naeel` — namespace в k8s (Fission)
- `wz03709-shturval-admin-naeel-test-3` — ещё один namespace
> ⚠️ `ClientID` в этом токене **отсутствует** — это другой issuer (`auth.k8s.ngcloud.ru`), не Keycloak.
### 5.2. Токен из HAR (deck-test)
| Поле | Значение |
|---|---|
| `iss` | `auth-api` |
| `ClientID` | `WZ01325` |
| `company_id` | `3e64aac6-dcfc-4082-88dc-da19c86555a5` |
| `company_name` | Тест |
| `login` | `tazet@narod.ru` |
| `firstname` | Наиль |
| `lastname` | Тазетдинов |
| `email` | `tazet@narod.ru` |
### 5.3. Целевой формат токена (из ТЗ)
| Claim | Назначение |
|---|---|
| `ClientID` | Идентификатор компании (WZ-номер) |
| `company_id` | UUID компании |
| `company_name` | Название компании |
| `email` | Email пользователя (для аудита) |
| `groups` | ? (пока null) |
| `roles` | ? (пока null) |
---
## 6. Роли
| Роль | ClientID | Права |
|---|---|---|
| **Администратор** | `WZ01112` | Все записи, просмотр аудита, экспорт |
| **Клиент** | любой другой WZ-номер | Только свои записи |
> Как именно ClientID попадает в токен — через группы Keycloak или маппер — **требует уточнения у DevOps**.
---
## 7. Реализация в коде
### `src/auth.js` — два режима
| Режим | Условие | Как работает |
|---|---|---|
| **OIDC** | `KC_CLIENT_ID` + `KC_CLIENT_SECRET` заданы | Authorization Code Flow, JWKS-валидация |
| **Mock** | `KC_CLIENT_ID` пусто | Локальная RSA-пара, JWT из `MOCK_USERS` |
### `src/config.js` — Mock-пользователи
```javascript
MOCK_USERS = [
{ clientId: 'WZ01325', companyName: 'Тест', email: 'tazet@narod.ru' },
{ clientId: 'WZ01112', companyName: 'Нубес', email: 'admin@nubes.ru' }
]
```
### Два слоя аутентификации
| Слой | Механизм | Где |
|---|---|---|
| **UI (EJS)** | Session cookie + PostgreSQL session store | `/`, `/admin`, `/entries` |
| **REST API** | Bearer JWT (всегда проверяется явно) | `/api/v1/*` |
### Dev-backdoor
- `DEV_MODE=true` или `DEV_SECRET``/dev-login` — выбор любого пользователя в обход OIDC/mock
---
## 8. Переменные окружения
```env
# Auth
DEV_MODE=true
JWT_ISSUER=mock-auth-api
ADMIN_CLIENT_ID=WZ01112
# OIDC (production)
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
KC_CLIENT_ID=
KC_CLIENT_SECRET=
APP_URL=http://localhost:3000
# Session
SESSION_SECRET=<обязателен в prod>
```
---
## 9. Открытые вопросы (к DevOps)
| # | Вопрос | Статус |
|---|---|---|
| 1 | Где брать `client_id` и `client_secret` для регистрации приложения в Keycloak? | 🔴 |
| 2 | Какой URL JWKS? `auth-api` или `keycloak.nubes.ru`? | 🔴 |
| 3 | Когда заполняются `claims.groups` и `claims.roles`? (пока null) | 🔴 |
| 4 | Как выглядит токен, если у пользователя несколько компаний? Массив `ClientID`? | 🔴 |
| 5 | Есть ли в Keycloak понятие admin-роли? В каком claim? | 🔴 |
| 6 | Redirect URI: `https://white.nodejsk8s.dev.nubes.ru/callback` — OK? | 🔴 |
| 7 | Кто потребитель `/export`? Какие IP/подсети для allowlist? | 🔴 |
| 8 | Формат `jsonEnv` — пример правильного? | 🔴 |
---
## 10. Исходные файлы
| Файл | Содержание |
|---|---|
| `docs/ТЗ.md` | Техническое задание |
| `docs/ТЗ-реализация.md` | Карта реализации требований |
| `docs/questions-devops.md` | Вопросы к DevOps |
| `research/auth-flow.md` | Детальное исследование OIDC-flow |
| `research/payg-sso/` | CF-эталон (OAuth2 + JWT RS256) |
| `Files/token.txt` | Реальный токен из test-окружения |
| `Files/nubes_login.har` | HAR трассировка login-сессии deck-test |
| `src/auth.js` | Основной модуль аутентификации |
| `src/config.js` | Конфигурация и mock-пользователи |
+104
View File
@@ -0,0 +1,104 @@
# Тесты: мульти-компания (2026-06-02)
> Запускать против `https://white.nodejsk8s.dev.nubes.ru`
> Куки сохраняются между запросами (использовать `-c jar.txt -b jar.txt`)
## 1. Логин под мульти-компанией
```bash
# Получить CSRF-токен и логин-форму
curl -s -c jar.txt https://white.nodejsk8s.dev.nubes.ru/login > /dev/null
# Извлечь CSRF-токен
CSRF=$(curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/login | grep -oP 'name="_csrf" value="\K[^"]+')
# Залогиниться как "multi"
curl -s -b jar.txt -c jar.txt \
-X POST https://white.nodejsk8s.dev.nubes.ru/login \
-d "user=multi&_csrf=$CSRF" \
-D - | head -1
# Ожидаем: HTTP/1.1 302 → /
# Проверить что сессия содержит allClientIds
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c 'WZ01325'
# Ожидаем: >0 (активная компания в интерфейсе)
```
## 2. Переключатель компаний в UI
```bash
# Страница должна содержать переключатель (<select name="switchTo">)
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c 'switchTo'
# Ожидаем: >0
# Должны быть обе компании в options
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -o 'WZ0[0-9]*'
# Ожидаем: WZ01325 и WZ02001
# Переключиться на WZ02001
curl -s -b jar.txt -c jar.txt \
https://white.nodejsk8s.dev.nubes.ru/?switchTo=WZ02001 \
> /dev/null
# Проверить что активная компания — WZ02001
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c 'WZ02001'
# Ожидаем: >0
```
## 3. Добавление записи в активную компанию
```bash
# Создать запись в WZ02001 (после переключения на неё)
CSRF=$(curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -oP 'name="_csrf" value="\K[^"]+')
curl -s -b jar.txt -c jar.txt \
-X POST https://white.nodejsk8s.dev.nubes.ru/add \
-d "value=8.8.8.8/32&comment=multi-test&_csrf=$CSRF" \
-D - | head -1
# Ожидаем: HTTP/1.1 302
# Проверить что запись появилась в WZ02001
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c '8.8.8.8'
# Ожидаем: >0
```
## 4. Проверка изоляции компаний
```bash
# Переключиться обратно на WZ01325
curl -s -b jar.txt -c jar.txt \
https://white.nodejsk8s.dev.nubes.ru/?switchTo=WZ01325 \
> /dev/null
# Запись 8.8.8.8 из WZ02001 НЕ должна быть видна в WZ01325
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c '8.8.8.8'
# Ожидаем: 0
```
## 5. API Bearer-тест (опционально)
```bash
# Выпустить mock JWT с clientid через запятую
# (требует ручной JWT через /dev-login или issueMockToken)
# Затем:
curl -s -H "Authorization: Bearer <mock-jwt>" \
https://white.nodejsk8s.dev.nubes.ru/api/v1/entries | jq .
# Ожидаем: записи для первой компании (WZ01325)
```
## 6. Негативные тесты
```bash
# Переключение на несуществующую компанию — игнорируется
curl -s -b jar.txt -c jar.txt \
https://white.nodejsk8s.dev.nubes.ru/?switchTo=WZ99999 \
> /dev/null
# Должен остаться на WZ01325
curl -s -b jar.txt https://white.nodejsk8s.dev.nubes.ru/ | grep -c 'WZ01325'
# Ожидаем: >0
# Одиночная компания (test, не multi) — переключателя быть не должно
# Залогиниться как test и проверить:
# grep -c 'switchTo' → ожидаем 0
```
+156
View File
@@ -0,0 +1,156 @@
# ТЗ-плюс — уточнения и дополнения от заказчика
> Основа: `docs/ТЗ.md`
> Файл для фиксации уточнений, дополнений и решений по мере обсуждения с заказчиком/DevOps.
> Каждая запись = дата + источник + формулировка + статус.
---
## 1. Аутентификация и Keycloak
### 1.1. client_id приложения в Keycloak
- **Дата**: 2026-06-02
- **Источник**: HAR `Files/nubes_login.har`
- **Факт**: `client_id = deck-test.ngcloud.ru` (в URL auth-запроса)
- **Статус**: 🔴 требует подтверждения — этот ли client_id для ipwhitelist, или будет отдельный?
### 1.2. ClientID в JWT — УТОЧНЕНО
- **ТЗ**: claim `ClientID` — идентификатор компании
- **Факт (HAR)**: `ClientID: "WZ01325"`, нет массива
- **Факт (token.txt)**: `ClientID` отсутствует, вместо него `claims[]` с названиями namespace'ов
- **Уточнение заказчика (01.06.2026)**: в claims приходит `clientid`. Формат может быть разный.
- Одно значение: `WZ04228`, `1700`, `asokolov-test`
- Несколько через запятую: `WZ11125, WZ03816`, `WZ52235, WZ62587, WZ02315`
- **Логика**: первое «слово» до запятой определяет общий ЛК.
Пользователи с одинаковым первым словом → общий whitelist.
- **Что делать в коде**: брать первый `clientid` до запятой как активную компанию.
Остальные (если есть) — дополнительные компании пользователя для переключателя.
- **Статус**: 🟡 уточнено, требует реализации в коде
### 1.3. Несколько компаний на пользователя — УТОЧНЕНО
- **ТЗ, п.3.1**: «поддерживается сценарий, когда пользователь принадлежит нескольким компаниям»
- **Уточнение заказчика (01.06.2026)**: поле `clientid` в claims содержит список через запятую.
Примеры: `WZ11125, WZ03816`, `WZ52235, WZ62587, WZ02315`.
- **Логика ЛК**: первое значение до запятой — активная компания.
Пользователи с одинаковым первым `clientid` видят общий whitelist.
Переключатель между компаниями — все значения из списка.
- **Открытый вопрос**: какое именно поле в Keycloak за это отвечает? Заказчик уточнит.
- **Статус**: 🟡 уточнено, требует реализации в коде
### 1.4. Admin-роль в Keycloak
- **ТЗ**: admin = `ClientID = WZ01112` + отдельный чек-бокс
- **Вопрос**: чек-бокс — это claim? Какой? `is_admin`, `realm_access.roles`, `groups`?
- **Статус**: 🔴
### 1.5. JWKS URL
- **Вариант A**: `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs`
- **Вариант B**: `https://auth-api...` (через API Gateway)
- **Статус**: 🔴
### 1.6. Redirect URI
- **Предложение**: `https://white.nodejsk8s.dev.nubes.ru/callback`
- **Статус**: 🔴 требует подтверждения DevOps
---
## 2. Инфраструктура и деплой
### 2.1. Платформа
- **Статус**: 🔴 уточнить — Node.js на k8s? Какой кластер?
### 2.2. Деплой
- **Статус**: 🔴 уточнить — CI/CD? Как поставлять `jsonEnv`?
### 2.3. Сетевое ограничение для /export
- **Вопрос**: какие IP/подсети будут потребителями внешней выдачи?
- **Статус**: 🔴
---
## 3. Функциональные уточнения
### 3.1. Мульти-компания в UI
- **ТЗ, п.3.1**: «переключатель активной компании»
- **Вопрос**: дизайн переключателя? Dropdown в шапке? Отдельная страница?
- **Статус**: 🔴
### 3.2. Soft-delete и фильтр «показать удалённые»
- **ТЗ, п.4.1**: «для администратора предусмотрен фильтр»
- **Код**: `includeDeleted` всегда `false`
- **Статус**: 🟡 не реализовано в коде
### 3.3. Индивидуальные лимиты компаний
- **ТЗ, п.4.5**: админ может задать индивидуальный лимит
- **Код**: ?
- **Статус**: 🔴 проверить реализацию
### 3.4. Уведомление о нормализации
- **ТЗ, п.5**: «пользователь должен быть уведомлен, что ввел адрес из хостовой части»
- **Статус**: 🔴 проверить реализацию в UI
---
## 4. Тестовые данные
### 4.1. Тестовые пользователи
| Email | ClientID | Компания | Роль |
|---|---|---|---|
| `tazet@narod.ru` | `WZ01325` | Тест | Клиент |
| `admin@nubes.ru` | `WZ01112` | Нубес | Админ |
| `tazetdinovn@gmail.com` | ? | ? | ? (из token.txt) |
### 4.2. HAR-сессия
- Файл: `Files/nubes_login.har`
- Пользователь: `tazet@narod.ru`
- Дата: 2026-05-30
- Окружение: deck-test
---
## 6. Уточнения от заказчика (01.06.2026)
### 6.1. Формат clientid в токене
- Поле: `clientid` в claims JWT
- Формат: строка, значения через запятую если несколько
- Примеры:
- `"WZ04228"` — одна компания
- `"1700"` — числовой ID
- `"asokolov-test"` — текстовый ID
- `"WZ11125, WZ03816"` — две компании
- `"WZ52235, WZ62587, WZ02315"` — три компании
- **Правило**: первое «слово» до запятой = активная компания (определяет ЛК)
- **Общий доступ**: пользователи с одинаковым первым словом имеют общий whitelist
### 6.2. Экспорт — подтверждение формата
- GET-endpoint отдаёт txt файл
- Одна строка = один объект (адрес/подсеть)
- Без разделителей, без заголовков
### 6.3. Текущее демо — одобрено
- `https://white.nodejsk8s.dev.nubes.ru/login` — ✓
- `https://white.nodejsk8s.dev.nubes.ru/exp` — ✓ (вывод без токена для тестирования)
### 6.4. Что ещё уточняется
- Какое именно поле Keycloak маппится в `clientid` claim? (Заказчик уточнит)
- Как определяется admin-роль? (отдельный чек-бокс в Keycloak?)
- `client_id` и `client_secret` для регистрации приложения
---
## 5. Решения и договорённости
> Сюда записывать принятые решения с датой и контекстом.
(пока пусто)
---
## Легенда статусов
| Статус | Значение |
|---|---|
| 🔴 | Требует уточнения |
| 🟡 | Уточнено, не реализовано |
| 🟢 | Реализовано / подтверждено |
| ⚫ | Отменено / не актуально |