diff --git a/packages/util/auth.go b/packages/util/auth.go index a40f6022..bf7300b9 100644 --- a/packages/util/auth.go +++ b/packages/util/auth.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" + "github.com/Infisical/infisical-merge/packages/config" infisicalSdk "github.com/infisical/go-sdk" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -60,6 +61,35 @@ func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, str return false, "" } +// resolveReLoginDomain decides which domain the automatic re-login child process +// should be pinned to, or returns "" to leave the child's normal domain +// resolution (and its interactive region prompt) untouched. +// +// storedDomain is LoggedInUserDomain from the config file, i.e. the instance +// whose session just expired, so it wins. resolvedURL is config.INFISICAL_URL as +// already resolved by the root command from --domain, INFISICAL_DOMAIN or +// .infisical.json; it is only forwarded when it differs from the built-in US +// default, since that default is what an unconfigured invocation looks like and +// forwarding it would silently skip the region prompt for first-time logins. +func resolveReLoginDomain(storedDomain string, resolvedURL string) string { + if storedDomain != "" { + return storedDomain + } + if resolvedURL != "" && resolvedURL != AppendAPIEndpoint(INFISICAL_DEFAULT_US_URL) { + return resolvedURL + } + return "" +} + +// buildReLoginArgs returns the argv for the re-login child process. +func buildReLoginArgs(domain string) []string { + args := []string{"login", "--silent"} + if domain != "" { + args = append(args, "--domain", domain) + } + return args +} + // EstablishUserLoginSession handles the login flow to either create a new session or restore an expired one. // It returns fresh user details if login is successful. func EstablishUserLoginSession() LoggedInUserDetails { @@ -70,8 +100,18 @@ func EstablishUserLoginSession() LoggedInUserDetails { PrintErrorMessageAndExit(fmt.Sprintf("Failed to determine executable path: %v", err)) } + // The child process resolves its own domain from scratch and would otherwise + // fall back to US Cloud, so forward the domain this session was using. + // Without this an expired EU Cloud session re-prompts for a region, opens the + // US login page, and on completion rewrites LoggedInUserDomain to US. + var storedDomain string + if configFile, configErr := GetConfigFile(); configErr == nil { + storedDomain = configFile.LoggedInUserDomain + } + domain := resolveReLoginDomain(storedDomain, config.INFISICAL_URL) + // Spawn infisical login command - loginCmd := exec.Command(exePath, "login", "--silent") + loginCmd := exec.Command(exePath, buildReLoginArgs(domain)...) loginCmd.Stdin = os.Stdin loginCmd.Stdout = os.Stdout loginCmd.Stderr = os.Stderr diff --git a/packages/util/auth_test.go b/packages/util/auth_test.go new file mode 100644 index 00000000..0c59afab --- /dev/null +++ b/packages/util/auth_test.go @@ -0,0 +1,98 @@ +package util + +import ( + "reflect" + "testing" +) + +func TestResolveReLoginDomain(t *testing.T) { + usDefault := AppendAPIEndpoint(INFISICAL_DEFAULT_US_URL) + euAPI := AppendAPIEndpoint(INFISICAL_DEFAULT_EU_URL) + + tests := []struct { + name string + storedDomain string + resolvedURL string + expected string + }{ + { + // The regression from the reported bug: an expired EU session must + // re-login against EU, not fall back to the US default. + name: "stored EU domain wins over US default", + storedDomain: euAPI, + resolvedURL: usDefault, + expected: euAPI, + }, + { + name: "stored self-hosted domain is forwarded", + storedDomain: "https://vault.example.com/api", + resolvedURL: usDefault, + expected: "https://vault.example.com/api", + }, + { + // --domain / INFISICAL_DOMAIN / .infisical.json, with nothing stored yet. + name: "explicit non-default resolved URL is forwarded when nothing is stored", + storedDomain: "", + resolvedURL: euAPI, + expected: euAPI, + }, + { + // Must stay empty: the US default is what an unconfigured invocation + // looks like, and forwarding it would skip the region prompt. + name: "bare US default is not forwarded", + storedDomain: "", + resolvedURL: usDefault, + expected: "", + }, + { + name: "nothing known at all", + storedDomain: "", + resolvedURL: "", + expected: "", + }, + { + // A user who explicitly logged in to US Cloud should stay pinned there + // rather than be re-prompted. + name: "stored US domain is still forwarded", + storedDomain: usDefault, + resolvedURL: usDefault, + expected: usDefault, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := resolveReLoginDomain(tt.storedDomain, tt.resolvedURL); got != tt.expected { + t.Errorf("resolveReLoginDomain(%q, %q) = %q, expected %q", + tt.storedDomain, tt.resolvedURL, got, tt.expected) + } + }) + } +} + +func TestBuildReLoginArgs(t *testing.T) { + tests := []struct { + name string + domain string + expected []string + }{ + { + name: "no domain keeps the original argv", + domain: "", + expected: []string{"login", "--silent"}, + }, + { + name: "domain is forwarded as an explicit flag", + domain: "https://eu.infisical.com/api", + expected: []string{"login", "--silent", "--domain", "https://eu.infisical.com/api"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := buildReLoginArgs(tt.domain); !reflect.DeepEqual(got, tt.expected) { + t.Errorf("buildReLoginArgs(%q) = %v, expected %v", tt.domain, got, tt.expected) + } + }) + } +}