diff --git a/shared-sqs/app/cmd/seed.go b/shared-sqs/app/cmd/seed.go index f54239a..8d36d49 100644 --- a/shared-sqs/app/cmd/seed.go +++ b/shared-sqs/app/cmd/seed.go @@ -1,7 +1,12 @@ // app/cmd/seed.go // Created: 2026-04-09 +// Updated: 2026-04-09 — фиксированные credentials для demo-tenant (BYOC) // Автосид демо-данных при старте через SHARED_SQS_SEED_DEMO=true. // Создаёт тенанта demo-service с 5 очередями и демо-сообщениями. +// +// DEMO CREDENTIALS — только для тестового стенда. +// Тенант demo-service изолирован: видит только свои очереди, не имеет доступа к +// admin API и к очередям других тенантов. Credentials открыты намеренно — стенд публичный. package main import ( @@ -16,10 +21,21 @@ import ( "shared-sqs/app/tenant" ) +// Фиксированные credentials демо-тенанта. +// Открыты намеренно — тестовый стенд. +// Тенант ограничен 10 очередями и не имеет прав admin API. +const ( + demoTenantID = "t-demo-shared-sqs-ngcloud" + demoAccessKey = "SSAK-demo-shared-sqs" + demoSecretKey = "demo-secret-key-shared-sqs-ngcloud-2026" + demoTenantName = "demo-service" + demoMaxQueues = 10 +) + // seedDemoData создаёт тенанта demo-service с очередями и сообщениями. // Вызывается при SHARED_SQS_SEED_DEMO=true при старте сервера. func seedDemoData(store *tenant.TenantStore) { - t, err := store.Create("demo-service", 10) + t, err := store.CreateFixed(demoTenantName, demoMaxQueues, demoTenantID, demoAccessKey, demoSecretKey) if err != nil { log.Warnf("seed: не удалось создать demo-tenant: %v", err) return diff --git a/shared-sqs/app/tenant/tenant_store.go b/shared-sqs/app/tenant/tenant_store.go index 3ce75b1..96c50c2 100644 --- a/shared-sqs/app/tenant/tenant_store.go +++ b/shared-sqs/app/tenant/tenant_store.go @@ -71,6 +71,31 @@ s.mu.Unlock() return t, nil } +// CreateFixed — создаёт тенанта с заранее известными credentials (для seed/demo). +// Используется только при инициализации демо-данных; не вызывается из user-facing API. +func (s *TenantStore) CreateFixed(name string, maxQueues int, tenantID, accessKey, secretKey string) (*Tenant, error) { +s.mu.Lock() +defer s.mu.Unlock() +if _, exists := s.byID[tenantID]; exists { +return nil, fmt.Errorf("tenant with id %s already exists", tenantID) +} +if _, exists := s.byAccessKey[accessKey]; exists { +return nil, fmt.Errorf("tenant with access key %s already exists", accessKey) +} +t := &Tenant{ +ID: tenantID, +Name: name, +AccessKey: accessKey, +SecretKey: secretKey, +MaxQueues: maxQueues, +CreatedAt: time.Now().UTC(), +Active: true, +} +s.byID[t.ID] = t +s.byAccessKey[t.AccessKey] = t +return t, nil +} + // GetByAccessKey — поиск тенанта по AccessKeyId (используется в auth middleware). func (s *TenantStore) GetByAccessKey(accessKey string) (*Tenant, bool) { s.mu.RLock() diff --git a/shared-sqs/doc/byoc-credentials.md b/shared-sqs/doc/byoc-credentials.md new file mode 100644 index 0000000..9778868 --- /dev/null +++ b/shared-sqs/doc/byoc-credentials.md @@ -0,0 +1,115 @@ +# BYOC — Bring Your Own Credentials + +**Дата**: 2026-04-09 +**Статус**: частично реализовано (backend), UI/API — TODO + +--- + +## Суть + +Возможность создать тенанта с **произвольными** Access Key и Secret Key вместо авто-генерируемых. + +Нужно для: +- демо-стенда с фиксированными credentials (README всегда актуален) +- интеграционных тестов с предсказуемыми значениями +- миграции с другого SQS-совместимого сервиса (сохранение существующих ключей) + +--- + +## Что уже сделано + +### `app/tenant/tenant_store.go` — `CreateFixed` + +```go +func (s *TenantStore) CreateFixed( + name string, + maxQueues int, + tenantID string, + accessKey string, + secretKey string, +) (*Tenant, error) +``` + +Создаёт тенанта с заранее известными credentials. +Проверяет уникальность и `tenantID`, и `accessKey` — конфликт возвращает ошибку. + +### `app/cmd/seed.go` — демо-тенант + +Использует `CreateFixed` при `SHARED_SQS_SEED_DEMO=true`: + +``` +tenantID = "t-demo-shared-sqs-ngcloud" +accessKey = "SSAK-demo-shared-sqs" +secretKey = "demo-secret-key-shared-sqs-ngcloud-2026" +``` + +--- + +## Что нужно сделать (TODO) + +### Admin API — `POST /admin/tenants` + +Добавить в `createTenantRequest` два опциональных поля: + +```go +// app/admin/admin.go +type createTenantRequest struct { + Name string `json:"name"` + MaxQueues int `json:"max_queues"` + AccessKey string `json:"access_key,omitempty"` // TODO: BYOC + SecretKey string `json:"secret_key,omitempty"` // TODO: BYOC +} +``` + +Логика в `createTenant` handler: + +```go +var t *tenant.Tenant +var err error +if req.AccessKey != "" || req.SecretKey != "" { + // BYOC: оба поля обязательны + if req.AccessKey == "" || req.SecretKey == "" { + jsonErr(w, http.StatusBadRequest, "both access_key and secret_key required when specifying custom credentials") + return + } + // Минимальная длина — защита от случайно слабых ключей + if len(req.AccessKey) < 8 || len(req.SecretKey) < 16 { + jsonErr(w, http.StatusBadRequest, "access_key min 8 chars, secret_key min 16 chars") + return + } + t, err = h.store.CreateFixed(req.Name, req.MaxQueues, generateTenantID(), req.AccessKey, req.SecretKey) +} else { + t, err = h.store.Create(req.Name, req.MaxQueues) +} +``` + +> `generateTenantID()` — уже есть в tenant_store.go, нужно экспортировать или вынести. + +### UI — Web форма создания тенанта + +- Добавить в модальное окно "Создать тенанта" два опциональных поля: Access Key, Secret Key +- Показывать только если нажата кнопка "задать свои credentials" +- Валидация на клиенте: оба поля заполнены, мин. длина + +--- + +## Безопасность + +- BYOC-credentials **не дают доступа к admin API** — admin защищён отдельным Bearer токеном +- Тенант видит **только свои очереди** — изоляция по AccessKey в auth middleware +- Слабые ключи отклоняются на уровне API (минимальная длина) +- Credentials передаются только по HTTPS + +--- + +## Демо-credentials (открыты намеренно) + +| | | +|---|---| +| **Access Key** | `SSAK-demo-shared-sqs` | +| **Secret Key** | `demo-secret-key-shared-sqs-ngcloud-2026` | +| **Tenant ID** | `t-demo-shared-sqs-ngcloud` | +| **Лимит очередей** | 10 | + +Эти credentials жёстко прописаны в `app/cmd/seed.go`. +Тенант создаётся только если `SHARED_SQS_SEED_DEMO=true` (env var в deployment.yaml).