From a6fc2d10b3fd7d383c3e18e050d8b7d6986084f1 Mon Sep 17 00:00:00 2001 From: "Bestony@Homelab" Date: Mon, 27 Jul 2026 17:08:41 +0800 Subject: [PATCH] feat(setup): add explicit setup bypass --- backend/internal/setup/setup.go | 15 +++++ backend/internal/setup/setup_test.go | 98 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go index b26acc87ff..a1d528927e 100644 --- a/backend/internal/setup/setup.go +++ b/backend/internal/setup/setup.go @@ -19,6 +19,7 @@ import ( _ "github.com/lib/pq" "github.com/redis/go-redis/v9" + "go.uber.org/zap" "gopkg.in/yaml.v3" ) @@ -147,9 +148,23 @@ func decideAdminBootstrap(totalUsers, adminUsers int64) adminBootstrapDecision { } } +func skipSetupEnabled() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("SKIP_SETUP"))) { + case "true", "1", "yes": + return true + default: + return false + } +} + // NeedsSetup checks if the system needs initial setup // Uses multiple checks to prevent attackers from forcing re-setup by deleting config func NeedsSetup() bool { + if skipSetupEnabled() { + logger.L().Debug("setup.needs_setup_bypassed", zap.String("reason", "skip_setup_enabled")) + return false + } + // Check 1: Config file must not exist if _, err := os.Stat(GetConfigFilePath()); !os.IsNotExist(err) { return false // Config exists, no setup needed diff --git a/backend/internal/setup/setup_test.go b/backend/internal/setup/setup_test.go index d1763d3adc..52f95e38c0 100644 --- a/backend/internal/setup/setup_test.go +++ b/backend/internal/setup/setup_test.go @@ -2,6 +2,7 @@ package setup import ( "os" + "path/filepath" "strings" "testing" "time" @@ -71,6 +72,103 @@ func TestSetupDefaultAdminConcurrency(t *testing.T) { }) } +func TestNeedsSetupSkipsWhenSkipSetupIsEnabled(t *testing.T) { + tests := []struct { + name string + value string + }{ + {name: "true", value: "true"}, + {name: "one", value: "1"}, + {name: "yes", value: "yes"}, + {name: "trimmed mixed case true", value: " TrUe "}, + {name: "trimmed mixed case yes", value: " YeS "}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("DATA_DIR", t.TempDir()) + t.Setenv("SKIP_SETUP", tc.value) + + if NeedsSetup() { + t.Fatalf("NeedsSetup() = true, want false when SKIP_SETUP is enabled") + } + }) + } +} + +func TestNeedsSetupFallsBackToFileDetectionWhenSkipSetupIsDisabled(t *testing.T) { + tests := []struct { + name string + skipSetupSet bool + skipSetup string + markerFile string + want bool + }{ + { + name: "unset without installation files", + want: true, + }, + { + name: "false without installation files", + skipSetupSet: true, + skipSetup: " false ", + want: true, + }, + { + name: "invalid value without installation files", + skipSetupSet: true, + skipSetup: "enabled", + want: true, + }, + { + name: "config file exists", + skipSetupSet: true, + skipSetup: "false", + markerFile: ConfigFileName, + want: false, + }, + { + name: "install lock file exists", + skipSetupSet: true, + skipSetup: "invalid", + markerFile: InstallLockFile, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dataDir := t.TempDir() + t.Setenv("DATA_DIR", dataDir) + if tc.skipSetupSet { + t.Setenv("SKIP_SETUP", tc.skipSetup) + } else { + originalValue, wasSet := os.LookupEnv("SKIP_SETUP") + if err := os.Unsetenv("SKIP_SETUP"); err != nil { + t.Fatalf("Unsetenv(SKIP_SETUP) error = %v", err) + } + t.Cleanup(func() { + if wasSet { + _ = os.Setenv("SKIP_SETUP", originalValue) + return + } + _ = os.Unsetenv("SKIP_SETUP") + }) + } + + if tc.markerFile != "" { + if err := os.WriteFile(filepath.Join(dataDir, tc.markerFile), nil, 0o600); err != nil { + t.Fatalf("WriteFile(%s) error = %v", tc.markerFile, err) + } + } + + if got := NeedsSetup(); got != tc.want { + t.Fatalf("NeedsSetup() = %v, want %v", got, tc.want) + } + }) + } +} + func TestSetupMigrationTimeout(t *testing.T) { t.Run("uses default timeout when unset", func(t *testing.T) { cfg := &SetupConfig{}