From 424ec32a6ad621259595f83bc1ae0afa30d40a66 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Tue, 6 Oct 2026 18:13:40 +0300 Subject: [PATCH] =?UTF-8?q?P1.5:=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=BC=D0=BE-=D0=B1=D1=8D=D0=BA=D0=B4=D0=BE=D1=80?= =?UTF-8?q?=20(=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D0=B9=20by?= =?UTF-8?q?pass-=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=20UI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удалены authenticateUIDemoToken, uiDemoToken/uiDemoTenantID/uiDemoEmail и их константы (demo-ui-shared-sqs-ngcloud-2026) — публичный путь входа в UI мимо JWT-валидации. seed.go теперь создаёт demo-тенанта со СЛУЧАЙНЫМИ кредами (tenant.Create), без захардкоженного SecretKey в исходнике. Тест TestJWTAuth_AllowsDemoToken заменён на TestJWTAuth_RejectsMissingToken. --- app/admin/admin.go | 86 ----------------------------------------- app/admin/admin_test.go | 31 +++------------ app/cmd/seed.go | 21 ++++------ 3 files changed, 13 insertions(+), 125 deletions(-) diff --git a/app/admin/admin.go b/app/admin/admin.go index b35e8ee..5a5f499 100644 --- a/app/admin/admin.go +++ b/app/admin/admin.go @@ -6,9 +6,7 @@ package admin import ( "context" - "crypto/subtle" "encoding/json" - "errors" "net/http" "os" "strings" @@ -30,17 +28,6 @@ const ( uiTenantContextKey uiContextKey = "ui-tenant" ) -// ВАЖНО: значения ниже относятся только к временному demo/showcase режиму. -// Перед production rollout без публичного demo-режима этот блок должен быть удалён -// вместе с веткой authenticateUIDemoToken и связанными UI-подсказками. -const ( - defaultUIDemoToken = "demo-ui-shared-sqs-ngcloud-2026" - defaultUIDemoTenantID = "t-demo-shared-sqs-ngcloud" - defaultUIDemoEmail = "demo@shared-sqs.ngcloud" -) - -var errUIDemoTokenMismatch = errors.New("demo token mismatch") - // ─── вспомогательная функция: найти очередь тенанта по имени ─────────────── // findQueue — возвращает ключ и очередь тенанта по имени, или "",nil если не найдено. func findQueue(tenantAccessKey, queueName string) (string, *models.Queue) { @@ -69,48 +56,6 @@ func NewHandler(store *tenant.TenantStore, adminToken string) *Handler { return &Handler{store: store, adminToken: adminToken, nubesEndpoint: nubesEndpoint} } -// uiDemoToken — возвращает публичный demo token для UI. -// Временный showcase-only путь: в production без demo user этот метод нужно удалить. -func (h *Handler) uiDemoToken() string { - if token := os.Getenv("SHARED_SQS_UI_DEMO_TOKEN"); token != "" { - return token - } - return defaultUIDemoToken -} - -// uiDemoTenantID — возвращает tenant ID, к которому привязан demo token. -// Существует только для showcase-режима, не для постоянной production auth-модели. -func (h *Handler) uiDemoTenantID() string { - if tenantID := os.Getenv("SHARED_SQS_UI_DEMO_TENANT_ID"); tenantID != "" { - return tenantID - } - return defaultUIDemoTenantID -} - -// uiDemoEmail — возвращает отображаемый email для demo UI session. -// Нужен только для публичного демо-логина и должен уйти вместе с demo path. -func (h *Handler) uiDemoEmail() string { - if email := os.Getenv("SHARED_SQS_UI_DEMO_EMAIL"); email != "" { - return email - } - return defaultUIDemoEmail -} - -// authenticateUIDemoToken — маппит публичный demo token на заранее сидированный demo tenant. -// Почему так: это быстрый showcase-вход для заказчика. Для production без demo user -// функция должна быть удалена, чтобы в коде не осталось публичного bypass-пути. -func (h *Handler) authenticateUIDemoToken(token string) (*tenant.Tenant, string, error) { - demoToken := h.uiDemoToken() - if demoToken == "" || subtle.ConstantTimeCompare([]byte(token), []byte(demoToken)) != 1 { - return nil, "", errUIDemoTokenMismatch - } - demoTenant, ok := h.store.GetByID(h.uiDemoTenantID()) - if !ok { - return nil, "", errors.New("demo tenant unavailable — enable SHARED_SQS_SEED_DEMO=true") - } - return demoTenant, h.uiDemoEmail(), nil -} - // currentUITenant — возвращает tenant, авторизованный через UI middleware. func currentUITenant(r *http.Request) (*tenant.Tenant, bool) { t, ok := r.Context().Value(uiTenantContextKey).(*tenant.Tenant) @@ -197,24 +142,6 @@ func (h *Handler) jwtAuth(w http.ResponseWriter, r *http.Request) { return } - if demoTenant, demoEmail, err := h.authenticateUIDemoToken(req.Token); err == nil { - log.Infof("ui auth: authenticated demo tenant=%s", demoTenant.ID) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "email": demoEmail, - "tenant_id": demoTenant.ID, - "access_key": demoTenant.AccessKey, - "secret_key": demoTenant.SecretKey, - "max_queues": demoTenant.MaxQueues, - "token": req.Token, - }) - return - } else if !errors.Is(err, errUIDemoTokenMismatch) { - log.Warnf("ui auth: demo login unavailable: %v", err) - jsonErr(w, http.StatusForbidden, err.Error()) - return - } - // Парсим JWT claims claims, err := auth.ParseJWTClaims(req.Token) if err != nil { @@ -305,19 +232,6 @@ func (h *Handler) jwtMiddleware(next http.Handler) http.Handler { } token := parts[1] - if demoTenant, _, err := h.authenticateUIDemoToken(token); err == nil { - if pathTenantID, exists := mux.Vars(r)["id"]; exists && pathTenantID != "" && pathTenantID != demoTenant.ID { - jsonErr(w, http.StatusForbidden, "forbidden tenant access") - return - } - ctx := context.WithValue(r.Context(), uiTenantContextKey, demoTenant) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } else if !errors.Is(err, errUIDemoTokenMismatch) { - jsonErr(w, http.StatusForbidden, err.Error()) - return - } - claims, err := auth.ParseJWTClaims(token) if err != nil { jsonErr(w, http.StatusUnauthorized, "invalid token: "+err.Error()) diff --git a/app/admin/admin_test.go b/app/admin/admin_test.go index 6b2c87b..97fdde9 100644 --- a/app/admin/admin_test.go +++ b/app/admin/admin_test.go @@ -1,7 +1,5 @@ // app/admin/admin_test.go // Focused tests for UI auth and scoping -// Created: 2026-04-12 09:28 MSK -// Updated: 2026-04-12 09:28 MSK — demo UI token and tenant-scoped UI responses package admin import ( @@ -15,37 +13,18 @@ import ( "shared-sqs/app/tenant" ) -// TestJWTAuth_AllowsDemoToken verifies that the public demo token authenticates into the seeded demo tenant. -func TestJWTAuth_AllowsDemoToken(t *testing.T) { - t.Setenv("SHARED_SQS_UI_DEMO_TOKEN", "demo-token-for-test") - t.Setenv("SHARED_SQS_UI_DEMO_TENANT_ID", "t-demo-test") - +// TestJWTAuth_RejectsMissingToken verifies jwtAuth rejects a request without a token. +func TestJWTAuth_RejectsMissingToken(t *testing.T) { store := tenant.NewTenantStore() - demoTenant, err := store.CreateFixed("demo-service", 10, "t-demo-test", "SSAK-demo-test", "demo-secret") - if err != nil { - t.Fatalf("CreateFixed(): %v", err) - } - h := NewHandler(store, "admin-token") - req := httptest.NewRequest(http.MethodPost, "/ui/api/auth", strings.NewReader(`{"token":"demo-token-for-test"}`)) + req := httptest.NewRequest(http.MethodPost, "/ui/api/auth", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() h.jwtAuth(w, req) - if w.Code != http.StatusOK { - t.Fatalf("jwtAuth() status = %d, body = %s", w.Code, w.Body.String()) - } - - var resp map[string]any - if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { - t.Fatalf("decode response: %v", err) - } - if got := resp["tenant_id"]; got != demoTenant.ID { - t.Fatalf("tenant_id = %v, want %s", got, demoTenant.ID) - } - if got := resp["access_key"]; got != demoTenant.AccessKey { - t.Fatalf("access_key = %v, want %s", got, demoTenant.AccessKey) + if w.Code != http.StatusBadRequest { + t.Fatalf("jwtAuth() status = %d, want 400", w.Code) } } diff --git a/app/cmd/seed.go b/app/cmd/seed.go index 34f6374..e318617 100644 --- a/app/cmd/seed.go +++ b/app/cmd/seed.go @@ -1,12 +1,11 @@ // app/cmd/seed.go -// Created: 2026-04-09 -// Updated: 2026-04-09 — фиксированные credentials для demo-tenant (BYOC) // Автосид демо-данных при старте через SHARED_SQS_SEED_DEMO=true. -// Создаёт тенанта demo-service с 5 очередями и демо-сообщениями. +// Создаёт тенанта demo-service со случайными кредами (генерируются через +// tenant.Create → crypto/rand) с 5 очередями и демо-сообщениями. // -// DEMO CREDENTIALS — только для тестового стенда. // Тенант demo-service изолирован: видит только свои очереди, не имеет доступа к -// admin API и к очередям других тенантов. Credentials открыты намеренно — стенд публичный. +// admin API и к очередям других тенантов. Креды генерируются случайно и печатаются +// в лог при старте (только при явном включении SHARED_SQS_SEED_DEMO). package main import ( @@ -21,26 +20,22 @@ import ( "shared-sqs/app/tenant" ) -// Фиксированные credentials демо-тенанта. -// Открыты намеренно — тестовый стенд. -// Тенант ограничен 10 очередями и не имеет прав admin API. const ( - demoTenantID = "t-demo-shared-sqs-ngcloud" - demoAccessKey = "SSAK-demo-shared-sqs" - demoSecretKey = "demo-secret-key-shared-sqs-ngcloud-2026" demoTenantName = "demo-service" demoMaxQueues = 10 ) // seedDemoData создаёт тенанта demo-service с очередями и сообщениями. // Вызывается при SHARED_SQS_SEED_DEMO=true при старте сервера. +// Креды генерируются случайно (tenant.Create) — никаких захардкоженных +// AccessKey/SecretKey в исходнике. func seedDemoData(store *tenant.TenantStore) { - t, err := store.CreateFixed(demoTenantName, demoMaxQueues, demoTenantID, demoAccessKey, demoSecretKey) + t, err := store.Create(demoTenantName, demoMaxQueues) if err != nil { log.Warnf("seed: не удалось создать demo-tenant: %v", err) return } - log.Infof("seed: создан тенант %s (AccessKey=%s)", t.ID, t.AccessKey) + log.Infof("seed: создан тенант %s (AccessKey=%s, SecretKey=%s)", t.ID, t.AccessKey, t.SecretKey) // Демо-очереди с набором сообщений queues := []struct {