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