From aa8968f2211811c82f455d92658ff73b85ff568a Mon Sep 17 00:00:00 2001 From: Naeel Date: Mon, 13 Apr 2026 06:06:56 +0300 Subject: [PATCH] doc: tenant isolation architecture --- doc/architecture/tenant-isolation.md | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 doc/architecture/tenant-isolation.md diff --git a/doc/architecture/tenant-isolation.md b/doc/architecture/tenant-isolation.md new file mode 100644 index 0000000..ddf8cc0 --- /dev/null +++ b/doc/architecture/tenant-isolation.md @@ -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) → доступ только к своим очередям +```