P0.1: случайные креды тенанта — убран детерминированный вывод из email
CreateFromJWT больше не выводит ID/AccessKey/SecretKey из email через SHA256 с захардкоженной солью (это позволяло вычислить чужие креды по публичному email). Креды генерируются случайно через crypto/rand. Удалены tenantIDFromEmail, accessKeyFromEmail, secretKeyFromEmail, secretKeySalt и ветка legacy-пересчёта. Идемпотентность по email сохранена (повторный логин возвращает тот же тенант).
This commit is contained in:
+36
-56
@@ -4,11 +4,9 @@ package tenant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ const DefaultTenantMaxQueues = 50
|
|||||||
// Tenant — модель тенанта shared-sqs.
|
// Tenant — модель тенанта shared-sqs.
|
||||||
// AccessKey используется как идентификатор в AWS Authorization header.
|
// AccessKey используется как идентификатор в AWS Authorization header.
|
||||||
type Tenant struct {
|
type Tenant struct {
|
||||||
ID string // уникальный идентификатор тенанта (t-<hex>: детерминированный из email для JWT-тенантов, случайный для manual)
|
ID string // уникальный идентификатор тенанта (t-<hex>: случайный, генерируется через crypto/rand)
|
||||||
Name string // человекочитаемое имя
|
Name string // человекочитаемое имя
|
||||||
AccessKey string // аналог AWS AccessKeyId (SSAK-<hex>)
|
AccessKey string // аналог AWS AccessKeyId (SSAK-<hex>)
|
||||||
SecretKey string // аналог AWS SecretAccessKey (64 hex chars)
|
SecretKey string // аналог AWS SecretAccessKey (64 hex chars)
|
||||||
@@ -196,54 +194,57 @@ func (s *TenantStore) GetByEmail(email string) (*Tenant, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateFromJWT — auto-provisioning тенанта из JWT claims.
|
// CreateFromJWT — auto-provisioning тенанта из JWT claims.
|
||||||
// Идентичность — ТОЛЬКО email: ID/AccessKey/SecretKey детерминированы из email.
|
// Идентичность — email: тенант ищется/создаётся по email (индекс byEmail).
|
||||||
// Тот же email на любом стенде → тот же тенант и те же креды. sub — информационное поле.
|
// Креды (ID/AccessKey/SecretKey) генерируются СЛУЧАЙНО через crypto/rand и
|
||||||
// Легаси-тенант (старые случайные ключи / другой ID) пересчитывается:
|
// НЕ выводятся из email — иначе креды можно было бы вычислить по публичному email
|
||||||
// старые индексы и Redis-запись удаляются, создаётся детерминированная запись.
|
// (SHA256(email+соль) при известной соли = полный доступ к чужим очередям).
|
||||||
// ВНИМАНИЕ: очереди привязаны к старому AccessKey — при пересчёте они остаются
|
// Тот же email при повторном логине → тот же тенант и те же креды (из индекса/Redis).
|
||||||
// в SyncQueues/Redis под старым префиксом (текущий тенант имеет 0 очередей).
|
|
||||||
func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant, error) {
|
func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant, error) {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return nil, fmt.Errorf("JWT email is required for tenant provisioning")
|
return nil, fmt.Errorf("JWT email is required for tenant provisioning")
|
||||||
}
|
}
|
||||||
tenantID := tenantIDFromEmail(email)
|
|
||||||
accessKey := accessKeyFromEmail(email)
|
|
||||||
secretKey := secretKeyFromEmail(email)
|
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
// Идемпотентность: тенант с таким email уже есть
|
// Идемпотентность: тенант с таким email уже есть — возвращаем как есть,
|
||||||
|
// креды НЕ пересчитываем и НЕ меняем (они случайны и неизменны).
|
||||||
if existing, ok := s.byEmail[email]; ok {
|
if existing, ok := s.byEmail[email]; ok {
|
||||||
// Полное совпадение — возвращаем как есть
|
if sub != "" {
|
||||||
if existing.ID == tenantID && existing.AccessKey == accessKey {
|
existing.NubesSub = sub
|
||||||
if sub != "" {
|
|
||||||
existing.NubesSub = sub
|
|
||||||
}
|
|
||||||
// Обновляем лимит очередей на актуальное значение из кода при каждом
|
|
||||||
// логине и персистим в Redis — иначе после рестарта пода из Redis
|
|
||||||
// восстановится старый лимит (было: тенант навсегда оставался с лимитом,
|
|
||||||
// заданным при первом логине).
|
|
||||||
if existing.MaxQueues != maxQueues {
|
|
||||||
existing.MaxQueues = maxQueues
|
|
||||||
if data, err := json.Marshal(existing); err == nil {
|
|
||||||
persistence.SaveTenantRaw(existing.ID, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return existing, nil
|
|
||||||
}
|
}
|
||||||
// Легаси: убираем старую запись из всех индексов и из Redis, ниже создаём новую
|
// Обновляем лимит очередей на актуальное значение из кода при каждом
|
||||||
delete(s.byID, existing.ID)
|
// логине и персистим в Redis — иначе после рестарта пода из Redis
|
||||||
delete(s.byAccessKey, existing.AccessKey)
|
// восстановится старый лимит (было: тенант навсегда оставался с лимитом,
|
||||||
delete(s.byEmail, existing.Email)
|
// заданным при первом логине).
|
||||||
persistence.DeleteTenant(existing.ID)
|
if existing.MaxQueues != maxQueues {
|
||||||
|
existing.MaxQueues = maxQueues
|
||||||
|
if data, err := json.Marshal(existing); err == nil {
|
||||||
|
persistence.SaveTenantRaw(existing.ID, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.byID) >= MaxTenantsGlobal {
|
if len(s.byID) >= MaxTenantsGlobal {
|
||||||
return nil, fmt.Errorf("global tenant limit reached (%d)", MaxTenantsGlobal)
|
return nil, fmt.Errorf("global tenant limit reached (%d)", MaxTenantsGlobal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
t := &Tenant{
|
||||||
ID: tenantID,
|
ID: id,
|
||||||
Name: email,
|
Name: email,
|
||||||
AccessKey: accessKey,
|
AccessKey: accessKey,
|
||||||
SecretKey: secretKey,
|
SecretKey: secretKey,
|
||||||
@@ -266,27 +267,6 @@ func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant,
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenantIDFromEmail — детерминированный ID тенанта из email: "t-{16 hex}".
|
|
||||||
func tenantIDFromEmail(email string) string {
|
|
||||||
hash := sha256.Sum256([]byte(strings.ToLower(email)))
|
|
||||||
return "t-" + hex.EncodeToString(hash[:8])
|
|
||||||
}
|
|
||||||
|
|
||||||
// accessKeyFromEmail — детерминированный AccessKey: "SSAK-{24 hex}".
|
|
||||||
func accessKeyFromEmail(email string) string {
|
|
||||||
hash := sha256.Sum256([]byte(strings.ToLower(email)))
|
|
||||||
return "SSAK-" + hex.EncodeToString(hash[:12])
|
|
||||||
}
|
|
||||||
|
|
||||||
// secretKeySalt — соль для детерминированного SecretKey email-тенантов.
|
|
||||||
const secretKeySalt = "shared-sqs:secret-key:v1"
|
|
||||||
|
|
||||||
// secretKeyFromEmail — детерминированный SecretKey: 64 hex = SHA256(email + соль).
|
|
||||||
func secretKeyFromEmail(email string) string {
|
|
||||||
h := sha256.Sum256([]byte(strings.ToLower(email) + ":" + secretKeySalt))
|
|
||||||
return hex.EncodeToString(h[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// List — список всех тенантов (для admin GET /tenants).
|
// List — список всех тенантов (для admin GET /tenants).
|
||||||
func (s *TenantStore) List() []*Tenant {
|
func (s *TenantStore) List() []*Tenant {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
|
|||||||
Reference in New Issue
Block a user