doc: tenant isolation architecture

This commit is contained in:
Naeel
2026-04-13 06:06:56 +03:00
parent 8fd0a59c41
commit aa8968f221
+96
View File
@@ -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) → доступ только к своим очередям
```