// Изменено: 2026-04-09 // Tenant model и in-memory хранилище тенантов для shared-sqs. package tenant import ( "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 } // TenantStore — потокобезопасное in-memory хранилище тенантов. // Два индекса позволяют быстро искать как по ID (admin API), так и по AccessKey (auth middleware). type TenantStore struct { 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), } } // 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) } 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() 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 } // 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 } // 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 } // 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 } // 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 } // 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 } // 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 }