fix(review): TOCTOU лимита тенантов + персист NubesSub + пакетный http.Client

1. TenantStore.Create: повторная проверка глобального лимита тенантов под Lock
   (ранняя была под RLock — гонка могла превысить MaxTenantsGlobal).
2. CreateFromJWT: обновлённый NubesSub теперь персистится в Redis (раньше
   сохранялся только внутри ветки смены MaxQueues и терялся при рестарте);
   запись в Redis только при реальном изменении.
3. auth.PingNubesAPI: общий http.Client (keep-alive) вместо нового на каждый
   вызов.
This commit is contained in:
Repinoid
2026-10-08 15:19:15 +03:00
parent 33a0adc9be
commit 572c617f44
2 changed files with 21 additions and 6 deletions
+5 -2
View File
@@ -78,6 +78,10 @@ var nubesAPIEndpoints = []string{
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
}
// nubesHTTPClient — общий клиент для валидации токенов: переиспользует
// соединения (keep-alive) вместо создания нового клиента на каждый запрос.
var nubesHTTPClient = &http.Client{Timeout: 10 * time.Second}
// PingNubesAPI — проверяет валидность токена перебором стендов Nubes API.
// endpoint (если задан через NUBES_ENDPOINT) пробуется первым.
// Логика: токен принимается ТОЛЬКО при явном HTTP 2xx от стенда.
@@ -104,8 +108,7 @@ func PingNubesAPI(ctx context.Context, endpoint, token string) error {
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "Mozilla/5.0")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
resp, err := nubesHTTPClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("nubes API unreachable at %s: %w", ep, err)
continue
+16 -4
View File
@@ -86,6 +86,12 @@ func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) {
}
s.mu.Lock()
// Повторная проверка лимита ПОД Lock: ранняя проверка выше была под RLock,
// между ней и взятием Lock другой поток мог создать тенанта (TOCTOU).
if len(s.byID) >= MaxTenantsGlobal {
s.mu.Unlock()
return nil, fmt.Errorf("global tenant limit reached (%d)", MaxTenantsGlobal)
}
s.byID[t.ID] = t
s.byAccessKey[t.AccessKey] = t
if t.Email != "" {
@@ -221,15 +227,21 @@ func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant,
// Идемпотентность: тенант с таким email уже есть — возвращаем как есть,
// креды НЕ пересчитываем и НЕ меняем (они случайны и неизменны).
if existing, ok := s.byEmail[email]; ok {
if sub != "" {
// Персистим только если что-то реально изменилось: иначе лишние записи
// в Redis на каждый логин, а обновлённый NubesSub раньше вообще терялся
// (сохранялся только внутри ветки смены MaxQueues).
changed := false
if sub != "" && existing.NubesSub != sub {
existing.NubesSub = sub
changed = true
}
// Обновляем лимит очередей на актуальное значение из кода при каждом
// логине и персистим в Redis — иначе после рестарта пода из Redis
// восстановится старый лимит (было: тенант навсегда оставался с лимитом,
// заданным при первом логине).
// логине — иначе после рестарта пода из Redis восстановится старый лимит.
if existing.MaxQueues != maxQueues {
existing.MaxQueues = maxQueues
changed = true
}
if changed {
if data, err := json.Marshal(existing); err == nil {
persistence.SaveTenantRaw(existing.ID, data)
}