doc: tenant isolation architecture
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# Разделение доступа пользователей (Tenant Isolation)
|
||||||
|
|
||||||
|
Дата: 2026-04-13
|
||||||
|
|
||||||
|
## Аутентификация (два пути)
|
||||||
|
|
||||||
|
### 1. AWS SigV4 (для SQS-клиентов)
|
||||||
|
|
||||||
|
Стандартная AWS-подпись. В заголовке `Authorization` передаётся `AccessKey` формата `SSAK-{hex}`, по нему находится тенант в `TenantStore`.
|
||||||
|
|
||||||
|
- `AccessKey`: `SSAK-{12 hex bytes}`
|
||||||
|
- `SecretKey`: `{64 hex chars}` (32 random bytes)
|
||||||
|
- Заголовок: `Authorization: AWS4-HMAC-SHA256 Credential={AccessKeyId}/{date}/{region}/sqs/aws4_request, SignedHeaders=..., Signature=...`
|
||||||
|
- Presigned URLs тоже поддерживаются (через `X-Amz-Credential` query parameter)
|
||||||
|
|
||||||
|
### 2. JWT (для UI-консоли)
|
||||||
|
|
||||||
|
Токен от облачной платформы Nubes. При первом входе тенант автоматически создаётся из JWT-клейма `sub`. Возвращаются `access_key` + `secret_key`.
|
||||||
|
|
||||||
|
Клеймы JWT:
|
||||||
|
- `sub` — UUID пользователя (используется для идентификации)
|
||||||
|
- `email` — email пользователя
|
||||||
|
- `exp` — время истечения
|
||||||
|
- `iss` — издатель
|
||||||
|
|
||||||
|
Генерация TenantID из JWT: `SHA256(sub) → первые 8 байт → hex → "sless-{16hex}"`.
|
||||||
|
|
||||||
|
Валидация подписи JWT не делается (trusted perimeter). Проверяется структура, `exp`, и через `PingNubesAPI()` — принимает ли облачный API этот токен.
|
||||||
|
|
||||||
|
## Изоляция очередей
|
||||||
|
|
||||||
|
Каждая очередь в памяти хранится с ключом `"{accessKey}:{queueName}"`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Очередь тенанта A: SSAK-aaa:my-queue
|
||||||
|
Очередь тенанта B: SSAK-bbb:my-queue
|
||||||
|
```
|
||||||
|
|
||||||
|
Все операции (SendMessage, ReceiveMessage, ListQueues и т.д.) строят ключ через `tenantQueueKey(tenant.AccessKey, queueName)` — тенант берётся из контекста запроса. Тенант A физически не может обратиться к ключу `SSAK-bbb:*`, потому что его AccessKey другой.
|
||||||
|
|
||||||
|
Файлы:
|
||||||
|
- `app/gosqs/tenant_helpers.go` — `tenantQueueKey()`, `countTenantQueues()`
|
||||||
|
- `app/tenant/tenant_store.go` — `TenantStore` с тремя индексами (`byID`, `byAccessKey`, `bySub`)
|
||||||
|
|
||||||
|
## TenantStore — три индекса
|
||||||
|
|
||||||
|
```
|
||||||
|
byID map[string]*Tenant — для admin API: GetByID(tenantID)
|
||||||
|
byAccessKey map[string]*Tenant — для SQS auth: GetByAccessKey(accessKey)
|
||||||
|
bySub map[string]*Tenant — для JWT auth: GetBySub(sub)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Защита от IDOR в UI
|
||||||
|
|
||||||
|
В `jwtMiddleware` (admin.go) проверяется что `tenantID` из URL совпадает с `tenantID` из JWT-токена. Если не совпадает — 403 Forbidden.
|
||||||
|
|
||||||
|
## Управление тенантами (admin API)
|
||||||
|
|
||||||
|
| Метод | Путь | Назначение | Доступ |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/admin/tenants` | Создать тенанта | Bearer token (admin) |
|
||||||
|
| GET | `/admin/tenants` | Список тенантов | Bearer или JWT (UI видит только себя) |
|
||||||
|
| GET | `/admin/tenants/{id}` | Детали тенанта | Bearer или JWT |
|
||||||
|
| DELETE | `/admin/tenants/{id}` | Удалить тенанта + все очереди | Bearer token (admin) |
|
||||||
|
| GET | `/admin/tenants/{id}/queues` | Очереди тенанта | Bearer или JWT |
|
||||||
|
|
||||||
|
`SecretKey` показывается только при создании. Потом получить нельзя.
|
||||||
|
|
||||||
|
## Роутинг и защита эндпоинтов
|
||||||
|
|
||||||
|
```
|
||||||
|
/health → PUBLIC
|
||||||
|
/metrics → PUBLIC
|
||||||
|
/admin/** → Bearer token auth
|
||||||
|
/ui/api/auth → PUBLIC (вход через JWT)
|
||||||
|
/ui/api/** → JWT auth
|
||||||
|
/ (SQS API) → AWS SigV4 auth
|
||||||
|
```
|
||||||
|
|
||||||
|
## Схема потока запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP запрос
|
||||||
|
↓
|
||||||
|
AuthMiddleware.extractAccessKeyID()
|
||||||
|
↓
|
||||||
|
TenantStore.GetByAccessKey(accessKey)
|
||||||
|
↓
|
||||||
|
verifySigV4 (проверка подписи)
|
||||||
|
↓
|
||||||
|
context.WithValue(TenantContextKey, tenant)
|
||||||
|
↓
|
||||||
|
actionHandler (SendMessage, ReceiveMessage, ...)
|
||||||
|
↓
|
||||||
|
tenantQueueKey(tenant.AccessKey, queueName) → доступ только к своим очередям
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user