diff --git a/apps/web/src/queries/settings.ts b/apps/web/src/queries/settings.ts index 497091b..52f1dd1 100644 --- a/apps/web/src/queries/settings.ts +++ b/apps/web/src/queries/settings.ts @@ -42,14 +42,12 @@ export const BOOLEAN_SETTING_KEYS = new Set(['runtime_logs_auto export const settingsKeys = { all: ['settings'] as const, - tenant: (tenantId: string) => [...settingsKeys.all, tenantId] as const, } -export function settingsQueryOptions(tenantId?: string | null) { +export function settingsQueryOptions() { return queryOptions({ - queryKey: settingsKeys.tenant(tenantId ?? ''), + queryKey: settingsKeys.all, queryFn: () => apiJSON('/v1/settings'), - enabled: Boolean(tenantId), staleTime: 30_000, }) } diff --git a/apps/web/src/routes/_auth/tenant-settings.tsx b/apps/web/src/routes/_auth/tenant-settings.tsx index 49393b1..76dc885 100644 --- a/apps/web/src/routes/_auth/tenant-settings.tsx +++ b/apps/web/src/routes/_auth/tenant-settings.tsx @@ -39,7 +39,6 @@ import { settingsQueryOptions, type BirdSettingKey, } from '@/queries/settings' -import { authSessionQueryOptions } from '@/queries/auth' import { apiMutate } from '@/lib/api-client' export const Route = createFileRoute('/_auth/tenant-settings')({ @@ -72,9 +71,7 @@ const BIRD_LABELS: Record = { function TenantSettingsComponent() { const search = useSearch({ from: '/_auth/tenant-settings' }) - const sessionQ = useQuery(authSessionQueryOptions()) - const tenantId = sessionQ.data?.tenant_id ?? null - const settingsQ = useQuery(settingsQueryOptions(tenantId)) + const settingsQ = useQuery(settingsQueryOptions()) const qc = useQueryClient() const partitioned = settingsQ.data ? partitionSettings(settingsQ.data) : null diff --git a/docs/access.md b/docs/access.md index 29e2a30..f6c8b04 100644 --- a/docs/access.md +++ b/docs/access.md @@ -50,7 +50,7 @@ opkey|01ARZ3NDEKTSV4RRFFQ69G5FAV|operator,nodekey|01ARZ3NDEKTSV4RRFFQ69G5FAV|nod Если в store доступен демо-tenant (`DemoIDs`, обычно `EVOBGP_SEED_DEMO` не равен `0`), заголовок **`Authorization: Bearer dev`** даёт роль **`operator`** для этого tenant. **Не зависит** от `EVOBGP_DEV_INSECURE`. -Если токен `dev` также задан в `EVOBGP_API_KEYS` или таблице `api_key`, **приоритет у явной записи** (production tenant), а не у demo-shortcut. +Без demo-tenant токен `dev` может быть задан в `EVOBGP_API_KEYS` (break-glass). **Запрещено** в продакшене: не оставляйте demo-seed с известным токеном `dev` на боевых данных. Переменная `EVOBGP_DEV_INSECURE` в текущей версии **не влияет** на аутентификацию (оставлена в compose для совместимости; не включайте в production — см. SEC-02 в инженерных правилах). diff --git a/internal/httpapi/auth.go b/internal/httpapi/auth.go index be57402..b331bfb 100644 --- a/internal/httpapi/auth.go +++ b/internal/httpapi/auth.go @@ -81,15 +81,16 @@ func authFromKeyRecord(raw string, rec apiKeyRecord) Auth { } // resolveAuth maps a bearer token to tenant identity. -// For the literal token "dev", env/DB keys take precedence over the demo shortcut (devAuth). +// For the literal token "dev", the demo shortcut (devAuth) takes precedence when demo-seed +// is available; env/DB mapping is used only when demo tenant is absent. func (s *Server) resolveAuth(raw string) (Auth, bool) { if raw == "dev" { - if rec, ok := s.keyResolver.Lookup(raw); ok { - return authFromKeyRecord(raw, rec), true - } if a, ok := s.devAuth(); ok { return a, true } + if rec, ok := s.keyResolver.Lookup(raw); ok { + return authFromKeyRecord(raw, rec), true + } return Auth{}, false } rec, ok := s.keyResolver.Lookup(raw) diff --git a/internal/httpapi/auth_dev_settings_test.go b/internal/httpapi/auth_dev_settings_test.go index a8073d5..d91ab13 100644 --- a/internal/httpapi/auth_dev_settings_test.go +++ b/internal/httpapi/auth_dev_settings_test.go @@ -31,7 +31,7 @@ func TestBearerDevGetSettings(t *testing.T) { } } -func TestBearerDevPrefersEnvAPIKeyOverDemoTenant(t *testing.T) { +func TestBearerDevPrefersDemoTenantOverEnvKey(t *testing.T) { srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed}) if err != nil { t.Fatal(err) @@ -61,7 +61,41 @@ func TestBearerDevPrefersEnvAPIKeyOverDemoTenant(t *testing.T) { t.Fatal(err) } got, _ := body["tenant_id"].(string) + if got != demoTenant { + t.Fatalf("tenant_id=%q want demo tenant %q (env=%q)", got, demoTenant, otherTenant) + } +} + +func TestBearerDevFallsBackToEnvKeyWithoutDemo(t *testing.T) { + srv, err := New(Options{SeedDemo: false, BundleSeedHex: testBundleSeed}) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + otherTenant := "00000000-0000-4000-8000-000000000001" + mustSetTestAPIKeys(t, srv, "dev|"+otherTenant+"|operator") + + ts := httptest.NewServer(srv.Handler()) + defer ts.Close() + + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/auth/session", nil) + req.Header.Set("Authorization", "Bearer dev") + resp, err := ts.Client().Do(req) + if err != nil { + t.Fatal(err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + b, _ := io.ReadAll(resp.Body) + t.Fatalf("session status=%d body=%s", resp.StatusCode, b) + } + var body map[string]any + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatal(err) + } + got, _ := body["tenant_id"].(string) if got != otherTenant { - t.Fatalf("tenant_id=%q want env key tenant %q (demo=%q)", got, otherTenant, demoTenant) + t.Fatalf("tenant_id=%q want env key tenant %q", got, otherTenant) } }