272 lines
9.6 KiB
Markdown
272 lines
9.6 KiB
Markdown
# Логика авторизации — Fission Console
|
||
|
||
> ⚠️ ЛЕГАСИ (актуально до ~апреля 2026): описание ниже относится к старой схеме с Deck API + JWT.
|
||
> Актуальное описание — **выше этого блока**, в разделе "АКТУАЛЬНО (май 2026+)".
|
||
|
||
---
|
||
|
||
# АКТУАЛЬНО (май 2026+)
|
||
|
||
## Что принимает сервер
|
||
|
||
Токен передаётся одним из двух способов:
|
||
- Заголовок `X-Auth-Token: <токен>`
|
||
- Заголовок `Authorization: Bearer <токен>`
|
||
|
||
`X-Auth-Env` — необязателен, по умолчанию `test`.
|
||
|
||
---
|
||
|
||
## Типы токенов и маршрутизация (MultiAuthenticator)
|
||
|
||
Сервер использует `MultiAuthenticator`, который определяет тип по форме токена:
|
||
|
||
| Форма токена | Authenticator | Описание |
|
||
|---|---|---|
|
||
| JWT (три части через `.`) | `DeckAuthenticator` или `TestAuthenticator` | Реальный облачный токен или тест-JWT |
|
||
| Любая строка ≥6 символов | `DemoAuthenticator` | Demo-логин без внешних запросов |
|
||
| Строка <6 символов | — | Ошибка 401 |
|
||
|
||
---
|
||
|
||
## DemoAuthenticator (текущий основной режим)
|
||
|
||
Принимает **любую строку ≥6 символов** как логин. Не ходит во внешние сервисы.
|
||
|
||
```
|
||
login (≥6 символов)
|
||
│
|
||
▼
|
||
Sub = UUID v5(login) ← детерминированный, фиксированный UUID namespace
|
||
Email = "demo-lo***in" ← маскированный для отображения в UI
|
||
│
|
||
▼
|
||
Namespace = "fission-" + hex(SHA256(Sub)[:8])
|
||
```
|
||
|
||
Пример:
|
||
- login = `livetest@test.local`
|
||
- Sub = `uuidV5("livetest@test.local")` = фиксированный UUID
|
||
- Namespace = `fission-c3fce59430e41b0f`
|
||
|
||
Каждый логин → один и тот же namespace (детерминированно).
|
||
|
||
---
|
||
|
||
## DeckAuthenticator (production JWT)
|
||
|
||
Для JWT-токенов из облака NUBES:
|
||
|
||
```
|
||
JWT токен
|
||
│
|
||
├─► validateToken → GET {deckAPI}/index.cfm/instances
|
||
│ Authorization: Bearer <token>
|
||
│ 401 → ошибка | другой → ОК
|
||
│ Кэш на 5 минут (sync.Map, ключ "env:token")
|
||
│
|
||
└─► identityFromJWT → decode payload (base64url, без верификации подписи)
|
||
→ claim "sub" → Sub
|
||
→ claim "email" → Email (опционально)
|
||
```
|
||
|
||
Deck API URL по стендам:
|
||
- `prod` → `https://deck-api.ngcloud.ru/api/v1`
|
||
- `dev` → `https://deck-api-dev.ngcloud.ru/api/v1`
|
||
- `test` → `https://deck-api-test.ngcloud.ru/api/v1`
|
||
|
||
---
|
||
|
||
## TestAuthenticator (тест-режим, устаревший)
|
||
|
||
Принимает JWT — декодирует sub из payload. Или строку с `@` — использует как sub напрямую.
|
||
Не ходит в Deck API. Включался через `FISSION_TEST_MODE=true`.
|
||
|
||
> ⚠️ Сейчас не используется — заменён на DemoAuthenticator.
|
||
|
||
---
|
||
|
||
## Вычисление namespace: `NamespaceForSub`
|
||
|
||
```go
|
||
func NamespaceForSub(sub string) string {
|
||
h := sha256.Sum256([]byte(sub))
|
||
return "fission-" + hex.EncodeToString(h[:8]) // 16 hex символов
|
||
}
|
||
```
|
||
|
||
Namespace **детерминирован**: одинаковый логин → всегда один namespace.
|
||
|
||
---
|
||
|
||
## authMiddleware — что происходит на каждый запрос
|
||
|
||
```
|
||
Входящий HTTP запрос
|
||
│
|
||
├─► authTokenFromRequest → X-Auth-Token || Authorization: Bearer
|
||
│
|
||
├─► authenticator.Authenticate(token, env) → UserIdentity{Sub, Email}
|
||
│ ошибка → 401
|
||
│
|
||
├─► NamespaceForSub(Sub) → "fission-XXXXXXXXXXXXXXXX"
|
||
│
|
||
├─► nsManager.EnsureUserNS(namespace)
|
||
│ → создаёт K8s namespace + RBAC если нет
|
||
│ ошибка → 502
|
||
│
|
||
└─► контекст запроса: ctxKeyNS=namespace, ctxKeyIdentity=identity
|
||
→ handler работает в namespace пользователя
|
||
```
|
||
|
||
---
|
||
|
||
## POST /auth — явный вход
|
||
|
||
```
|
||
POST /console/api/auth
|
||
Body: { "token": "...", "env": "test" }
|
||
│
|
||
└─► то же что authMiddleware, возвращает:
|
||
{ ok: true, namespace: "fission-...", email: "demo-lo***in" }
|
||
```
|
||
|
||
---
|
||
|
||
## Итого — полная схема (актуальная)
|
||
|
||
```
|
||
Пользователь передаёт токен/логин
|
||
│
|
||
▼
|
||
MultiAuthenticator
|
||
├── JWT? → DeckAuthenticator (Deck API + JWT decode)
|
||
└── нет → DemoAuthenticator (≥6 символов → UUID v5)
|
||
│
|
||
▼
|
||
UserIdentity { Sub, Email }
|
||
│
|
||
▼
|
||
NamespaceForSub(Sub) = "fission-" + hex(SHA256(Sub)[:8])
|
||
│
|
||
▼
|
||
EnsureUserNS → K8s namespace существует
|
||
│
|
||
▼
|
||
Все хендлеры работают с namespace из context
|
||
```
|
||
|
||
---
|
||
|
||
# ЛЕГАСИ (до апреля 2026) — не удалять, для анализа
|
||
|
||
|
||
|
||
## Что вводит пользователь
|
||
|
||
В форме логина (`index.html`, overlay `#login-overlay`):
|
||
- **Стенд** (`#l-env`): `dev` / `test` / `prod`
|
||
- **Токен** (`#l-token`): JWT-токен из личного кабинета NUBES (Профиль → Токены)
|
||
|
||
---
|
||
|
||
## Фронтенд: `doLogin()` → `js/auth.js`
|
||
|
||
1. Берёт токен и стенд из формы
|
||
2. Делает `POST /console/api/auth` с `{ token, env }` в теле
|
||
3. Если ответ OK:
|
||
- Сохраняет `auth_token` и `auth_env` в **localStorage** браузера
|
||
- Вызывает `setUserAvatar(token)` — декодирует email из JWT payload и показывает в navbar
|
||
- Скрывает overlay логина
|
||
4. Проверяет `GET /console/api/ns/status` — если namespace не готов, показывает overlay инициализации
|
||
|
||
---
|
||
|
||
## Бэкенд: `handleAuth` → `handlers.go:1022`
|
||
|
||
1. Принимает `POST /console/api/auth`
|
||
2. Валидирует токен через `resolveNamespaceForToken(token, env, testMode)`
|
||
3. Вызывает `EnsureUserNS` — гарантирует что K8s namespace + RBAC + quota существуют
|
||
4. Возвращает JSON: `{ ok: true, env: "test", namespace: "fission-..." }`
|
||
|
||
---
|
||
|
||
## Валидация токена: `validateDeckToken` → `server.go:279`
|
||
|
||
**Deck API** — внешний сервис облачной платформы NUBES:
|
||
```
|
||
dev → https://deck-api-dev.ngcloud.ru/api/v1
|
||
test → https://deck-api-test.ngcloud.ru/api/v1
|
||
prod → https://deck-api.ngcloud.ru/api/v1
|
||
```
|
||
|
||
Проверка: `GET {deckAPI}/index.cfm/instances` с заголовком `Authorization: Bearer {token}`
|
||
- `401` → токен невалиден
|
||
- Любой другой ответ → токен принят
|
||
- Результат кэшируется на **5 минут** (`sync.Map`)
|
||
|
||
---
|
||
|
||
## Вычисление namespace: `namespaceFromJWT` → `auth.go:131`
|
||
|
||
1. JWT токен **не верифицируется по подписи** — Deck API уже это сделал
|
||
2. Декодируется payload (base64url)
|
||
3. Извлекается claim `"sub"` (идентификатор пользователя в облаке)
|
||
4. Namespace = `"fission-" + hex(SHA256(sub)[:8])`
|
||
|
||
Пример: sub = `c3fce59430e41b0f...` → namespace `fission-c3fce59430e41b0f`
|
||
|
||
---
|
||
|
||
## Email в navbar: `setUserAvatar` / `jwtEmail` → `auth.js`
|
||
|
||
- `jwtEmail(token)` — декодирует JWT payload на **фронтенде** (без обращения к серверу)
|
||
- Берёт `payload.email` или `payload.sub`
|
||
- Отображает в `#user-avatar` в левом верхнем углу (вместо синего кружка)
|
||
|
||
---
|
||
|
||
## Последующие запросы
|
||
|
||
Каждый API-запрос от фронтенда включает заголовки:
|
||
```
|
||
X-Auth-Token: <токен из localStorage>
|
||
X-Auth-Env: <стенд из localStorage>
|
||
```
|
||
|
||
`authMiddleware` (`auth.go`) снова валидирует токен (но из кэша — без нового HTTP-запроса к Deck API),
|
||
вычисляет namespace и кладёт его в `context.Context` → все хендлеры работают в нужном namespace.
|
||
|
||
---
|
||
|
||
## Test-режим (FISSION_TEST_MODE=true)
|
||
|
||
Вместо реального Deck API — проверяет заголовок `X-Test-Sub`.
|
||
Если токен содержит `@` — трактуется как email и используется как sub для вычисления namespace.
|
||
Используется в тестах и для live-тестирования без реальных токенов.
|
||
|
||
---
|
||
|
||
## Итого — схема
|
||
|
||
```
|
||
Пользователь вводит токен + стенд
|
||
│
|
||
▼
|
||
POST /console/api/auth
|
||
│
|
||
├─► validateDeckToken → Deck API ({env}.ngcloud.ru) → 200 = ОК
|
||
│
|
||
├─► namespaceFromJWT → decode JWT payload → claim "sub" → SHA256[:8] → "fission-XXXXXXXX"
|
||
│
|
||
├─► EnsureUserNS → K8s namespace + RBAC создаются если нет
|
||
│
|
||
└─► { ok: true, namespace: "fission-..." }
|
||
|
||
Фронтенд:
|
||
├─► localStorage.setItem('auth_token', token)
|
||
├─► localStorage.setItem('auth_env', env)
|
||
├─► jwtEmail(token) → email из JWT payload → показать в navbar
|
||
└─► reloadAll() — загрузить функции
|
||
```
|