diff --git a/shared-sqs/app/tenant/tenant_store.go b/shared-sqs/app/tenant/tenant_store.go index 96c50c2..0810b69 100644 --- a/shared-sqs/app/tenant/tenant_store.go +++ b/shared-sqs/app/tenant/tenant_store.go @@ -3,165 +3,165 @@ package tenant import ( -"crypto/rand" -"encoding/hex" -"fmt" -"sync" -"time" + "crypto/rand" + "encoding/hex" + "fmt" + "sync" + "time" ) // Tenant — модель тенанта shared-sqs. // AccessKey используется как идентификатор в AWS Authorization header. type Tenant struct { -ID string // уникальный идентификатор тенанта (t-) -Name string // человекочитаемое имя -AccessKey string // аналог AWS AccessKeyId (SSAK-) -SecretKey string // аналог AWS SecretAccessKey (64 hex chars) -MaxQueues int // лимит очередей (0 = безлимит) -CreatedAt time.Time -Active bool + ID string // уникальный идентификатор тенанта (t-) + Name string // человекочитаемое имя + AccessKey string // аналог AWS AccessKeyId (SSAK-) + SecretKey string // аналог AWS SecretAccessKey (64 hex chars) + MaxQueues int // лимит очередей (0 = безлимит) + CreatedAt time.Time + Active bool } // TenantStore — потокобезопасное in-memory хранилище тенантов. // Два индекса позволяют быстро искать как по ID (admin API), так и по AccessKey (auth middleware). type TenantStore struct { -mu sync.RWMutex -byID map[string]*Tenant -byAccessKey map[string]*Tenant + mu sync.RWMutex + byID map[string]*Tenant + byAccessKey map[string]*Tenant } // NewTenantStore — создаёт пустое хранилище тенантов. func NewTenantStore() *TenantStore { -return &TenantStore{ -byID: make(map[string]*Tenant), -byAccessKey: make(map[string]*Tenant), -} + return &TenantStore{ + byID: make(map[string]*Tenant), + byAccessKey: make(map[string]*Tenant), + } } // Create — создаёт нового тенанта, генерирует ключи, сохраняет в оба индекса. func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { -id, err := generateTenantID() -if err != nil { -return nil, fmt.Errorf("generate tenant id: %w", err) -} -accessKey, err := generateAccessKey() -if err != nil { -return nil, fmt.Errorf("generate access key: %w", err) -} -secretKey, err := generateSecretKey() -if err != nil { -return nil, fmt.Errorf("generate secret key: %w", err) -} + id, err := generateTenantID() + if err != nil { + return nil, fmt.Errorf("generate tenant id: %w", err) + } + accessKey, err := generateAccessKey() + if err != nil { + return nil, fmt.Errorf("generate access key: %w", err) + } + secretKey, err := generateSecretKey() + if err != nil { + return nil, fmt.Errorf("generate secret key: %w", err) + } -t := &Tenant{ -ID: id, -Name: name, -AccessKey: accessKey, -SecretKey: secretKey, -MaxQueues: maxQueues, -CreatedAt: time.Now().UTC(), -Active: true, -} + t := &Tenant{ + ID: id, + Name: name, + AccessKey: accessKey, + SecretKey: secretKey, + MaxQueues: maxQueues, + CreatedAt: time.Now().UTC(), + Active: true, + } -s.mu.Lock() -s.byID[t.ID] = t -s.byAccessKey[t.AccessKey] = t -s.mu.Unlock() + s.mu.Lock() + s.byID[t.ID] = t + s.byAccessKey[t.AccessKey] = t + s.mu.Unlock() -return t, nil + 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 + 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() -t, ok := s.byAccessKey[accessKey] -s.mu.RUnlock() -return t, ok + s.mu.RLock() + t, ok := s.byAccessKey[accessKey] + s.mu.RUnlock() + return t, ok } // GetByID — поиск тенанта по ID (используется в admin API). func (s *TenantStore) GetByID(id string) (*Tenant, bool) { -s.mu.RLock() -t, ok := s.byID[id] -s.mu.RUnlock() -return t, ok + s.mu.RLock() + t, ok := s.byID[id] + s.mu.RUnlock() + return t, ok } // Delete — удаляет тенанта из ОБОИХ индексов. // Ловушка #2: если удалить только из одного индекса — orphaned данные и memory leak. func (s *TenantStore) Delete(id string) bool { -s.mu.Lock() -defer s.mu.Unlock() -t, ok := s.byID[id] -if !ok { -return false -} -delete(s.byID, t.ID) -delete(s.byAccessKey, t.AccessKey) -return true + s.mu.Lock() + defer s.mu.Unlock() + t, ok := s.byID[id] + if !ok { + return false + } + delete(s.byID, t.ID) + delete(s.byAccessKey, t.AccessKey) + return true } // List — список всех тенантов (для admin GET /tenants). func (s *TenantStore) List() []*Tenant { -s.mu.RLock() -result := make([]*Tenant, 0, len(s.byID)) -for _, t := range s.byID { -result = append(result, t) -} -s.mu.RUnlock() -return result + s.mu.RLock() + result := make([]*Tenant, 0, len(s.byID)) + for _, t := range s.byID { + result = append(result, t) + } + s.mu.RUnlock() + return result } // generateTenantID — генерирует уникальный ID тенанта в формате t-<12 hex bytes>. func generateTenantID() (string, error) { -b := make([]byte, 8) -if _, err := rand.Read(b); err != nil { -return "", err -} -return "t-" + hex.EncodeToString(b), nil + b := make([]byte, 8) + if _, err := rand.Read(b); err != nil { + return "", err + } + return "t-" + hex.EncodeToString(b), nil } // generateAccessKey — генерирует AccessKey в формате SSAK-<12 hex bytes>. // SSAK = Shared SQS Access Key. Используем crypto/rand (ловушка #1: не math/rand). func generateAccessKey() (string, error) { -b := make([]byte, 12) -if _, err := rand.Read(b); err != nil { -return "", err -} -return "SSAK-" + hex.EncodeToString(b), nil + b := make([]byte, 12) + if _, err := rand.Read(b); err != nil { + return "", err + } + return "SSAK-" + hex.EncodeToString(b), nil } // generateSecretKey — генерирует SecretKey как 64 hex символа (32 random bytes). // Используем crypto/rand (ловушка #1). func generateSecretKey() (string, error) { -b := make([]byte, 32) -if _, err := rand.Read(b); err != nil { -return "", err -} -return hex.EncodeToString(b), nil + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil }