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:
+5
-2
@@ -78,6 +78,10 @@ var nubesAPIEndpoints = []string{
|
|||||||
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nubesHTTPClient — общий клиент для валидации токенов: переиспользует
|
||||||
|
// соединения (keep-alive) вместо создания нового клиента на каждый запрос.
|
||||||
|
var nubesHTTPClient = &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
// PingNubesAPI — проверяет валидность токена перебором стендов Nubes API.
|
// PingNubesAPI — проверяет валидность токена перебором стендов Nubes API.
|
||||||
// endpoint (если задан через NUBES_ENDPOINT) пробуется первым.
|
// endpoint (если задан через NUBES_ENDPOINT) пробуется первым.
|
||||||
// Логика: токен принимается ТОЛЬКО при явном HTTP 2xx от стенда.
|
// Логика: токен принимается ТОЛЬКО при явном HTTP 2xx от стенда.
|
||||||
@@ -104,8 +108,7 @@ func PingNubesAPI(ctx context.Context, endpoint, token string) error {
|
|||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
resp, err := nubesHTTPClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("nubes API unreachable at %s: %w", ep, err)
|
lastErr = fmt.Errorf("nubes API unreachable at %s: %w", ep, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
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.byID[t.ID] = t
|
||||||
s.byAccessKey[t.AccessKey] = t
|
s.byAccessKey[t.AccessKey] = t
|
||||||
if t.Email != "" {
|
if t.Email != "" {
|
||||||
@@ -221,15 +227,21 @@ func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant,
|
|||||||
// Идемпотентность: тенант с таким email уже есть — возвращаем как есть,
|
// Идемпотентность: тенант с таким email уже есть — возвращаем как есть,
|
||||||
// креды НЕ пересчитываем и НЕ меняем (они случайны и неизменны).
|
// креды НЕ пересчитываем и НЕ меняем (они случайны и неизменны).
|
||||||
if existing, ok := s.byEmail[email]; ok {
|
if existing, ok := s.byEmail[email]; ok {
|
||||||
if sub != "" {
|
// Персистим только если что-то реально изменилось: иначе лишние записи
|
||||||
|
// в Redis на каждый логин, а обновлённый NubesSub раньше вообще терялся
|
||||||
|
// (сохранялся только внутри ветки смены MaxQueues).
|
||||||
|
changed := false
|
||||||
|
if sub != "" && existing.NubesSub != sub {
|
||||||
existing.NubesSub = sub
|
existing.NubesSub = sub
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
// Обновляем лимит очередей на актуальное значение из кода при каждом
|
// Обновляем лимит очередей на актуальное значение из кода при каждом
|
||||||
// логине и персистим в Redis — иначе после рестарта пода из Redis
|
// логине — иначе после рестарта пода из Redis восстановится старый лимит.
|
||||||
// восстановится старый лимит (было: тенант навсегда оставался с лимитом,
|
|
||||||
// заданным при первом логине).
|
|
||||||
if existing.MaxQueues != maxQueues {
|
if existing.MaxQueues != maxQueues {
|
||||||
existing.MaxQueues = maxQueues
|
existing.MaxQueues = maxQueues
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
if data, err := json.Marshal(existing); err == nil {
|
if data, err := json.Marshal(existing); err == nil {
|
||||||
persistence.SaveTenantRaw(existing.ID, data)
|
persistence.SaveTenantRaw(existing.ID, data)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user