From 7b5c9ad9c6971f5a36c5e074b7b7e0fedbb3888c Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 11 Oct 2026 21:52:36 +0200 Subject: [PATCH] fix(auth): reset other credential helpers in auth setup-git setup-git appended tea's helper after the helpers Git already had, such as osxkeychain from the system configuration. Those helpers answered first, and after a push Git asked every helper to store tea's token, so it ended up in the macOS Keychain where auth logout never removes it. setup-git now writes an empty helper for the instance before tea's own, as tea already does for its per-operation Git credentials. Add --keep-helpers for the previous behavior. Signed-off-by: bircni --- internal/app/git_credentials.go | 5 ++-- internal/git/credentials.go | 18 +++++++++++--- internal/git/credentials_test.go | 41 ++++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/internal/app/git_credentials.go b/internal/app/git_credentials.go index 2aa0817a..2d502a94 100644 --- a/internal/app/git_credentials.go +++ b/internal/app/git_credentials.go @@ -106,7 +106,7 @@ func (a *application) gitCredentialCommand() *cobra.Command { } func (a *application) setupGitCommand() *cobra.Command { - var local bool + var local, keepHelpers bool command := &cobra.Command{Use: "setup-git", Short: "Explicitly register tea as a scoped Git credential helper", Annotations: textOutput("setup-git configures Git"), Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error { target, err := a.resolveTarget(cmd.Context(), false, false) if err != nil { @@ -117,9 +117,10 @@ func (a *application) setupGitCommand() *cobra.Command { return err } pinned, _ := a.selectedUser() - return git.SetupCredentials(cmd.Context(), a.deps.Git, git.CredentialSetup{Directory: a.options.cwd, Local: local, Instance: target.Host, Executable: executable, User: pinned, ConfigDirectory: configDir}) + return git.SetupCredentials(cmd.Context(), a.deps.Git, git.CredentialSetup{Directory: a.options.cwd, Local: local, KeepHelpers: keepHelpers, Instance: target.Host, Executable: executable, User: pinned, ConfigDirectory: configDir}) }} command.Flags().BoolVar(&local, "local", false, "Configure only the current Git repository instead of global Git configuration") + command.Flags().BoolVar(&keepHelpers, "keep-helpers", false, "Keep other credential helpers for the instance; they are asked first and store tea's token") return command } diff --git a/internal/git/credentials.go b/internal/git/credentials.go index a3faaab9..5009cf51 100644 --- a/internal/git/credentials.go +++ b/internal/git/credentials.go @@ -57,9 +57,12 @@ func CredentialArguments(instance, executable, user, configDir string) ([]string type CredentialSetup struct { Directory, Instance, Executable, User, ConfigDirectory string Local bool + // KeepHelpers adds tea's helper after other helpers instead of resetting them for the instance. + KeepHelpers bool } -// SetupCredentials adds a scoped helper without removing other credential helpers. +// SetupCredentials installs a helper scoped to the instance. Unless KeepHelpers is set, an empty +// helper first resets other helpers for the instance, so they neither answer for tea nor store its token. func SetupCredentials(ctx context.Context, runner Runner, options CredentialSetup) error { normalized, err := settings.NormalizeHost(options.Instance) if err != nil { @@ -100,8 +103,17 @@ func SetupCredentials(ctx context.Context, runner Runner, options CredentialSetu return err } } - if slices.Contains(helpers, helper) { - return nil + if options.KeepHelpers { + if slices.Contains(helpers, helper) { + return nil + } + } else { + if slices.Equal(helpers, []string{"", helper}) { + return nil + } + if _, err := runner.Output(ctx, cwd, false, "config", scope, "--replace-all", prefix+"helper", ""); err != nil { + return err + } } _, err = runner.Output(ctx, cwd, false, "config", scope, "--add", prefix+"helper", helper) return err diff --git a/internal/git/credentials_test.go b/internal/git/credentials_test.go index 1973fc91..548da2ff 100644 --- a/internal/git/credentials_test.go +++ b/internal/git/credentials_test.go @@ -26,8 +26,9 @@ func TestSetupGitCredentialsPreservesHelpersAndIsIdempotent(t *testing.T) { _, err = runner.Output(ctx, cwd, false, "config", "--local", "--add", key, "existing-helper") require.NoError(t, err) executable := filepath.Join(cwd, "path with spaces and 'quote'", "tea") - require.NoError(t, SetupCredentials(ctx, runner, CredentialSetup{Directory: cwd, Local: true, Instance: instance, Executable: executable, User: "alice"})) - require.NoError(t, SetupCredentials(ctx, runner, CredentialSetup{Directory: cwd, Local: true, Instance: instance, Executable: executable, User: "alice"})) + setup := CredentialSetup{Directory: cwd, Local: true, KeepHelpers: true, Instance: instance, Executable: executable, User: "alice"} + require.NoError(t, SetupCredentials(ctx, runner, setup)) + require.NoError(t, SetupCredentials(ctx, runner, setup)) data, err := runner.Output(ctx, cwd, false, "config", "--local", "--get-all", key) require.NoError(t, err) helpers := strings.Split(data, "\n") @@ -41,6 +42,42 @@ func TestSetupGitCredentialsPreservesHelpersAndIsIdempotent(t *testing.T) { require.Equal(t, "true", data) } +func TestSetupGitCredentialsResetsOtherHelpersForTheInstance(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("Git is not installed") + } + t.Setenv("GIT_CONFIG_NOSYSTEM", "1") + t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(t.TempDir(), "gitconfig")) + t.Setenv("GIT_TERMINAL_PROMPT", "0") + ctx := context.Background() + cwd := t.TempDir() + runner := System{} + _, err := runner.Output(ctx, cwd, false, "init") + require.NoError(t, err) + instance := "https://example.test:8443/gitea" + key := "credential." + instance + ".helper" + for _, helper := range [][2]string{{"credential.helper", `!f() { test "$1" = get && printf 'username=other\npassword=leaked\n'; }; f`}, {key, "stale-helper"}} { + _, err = runner.Output(ctx, cwd, false, "config", "--local", "--add", helper[0], helper[1]) + require.NoError(t, err) + } + executable := filepath.Join(t.TempDir(), "tea") + require.NoError(t, os.WriteFile(executable, []byte("#!/bin/sh\nfor last; do :; done\nif [ \"$last\" = get ]; then printf 'username=tea\\npassword=tea-token\\n'; fi\n"), 0o755)) + setup := CredentialSetup{Directory: cwd, Local: true, Instance: instance, Executable: executable} + require.NoError(t, SetupCredentials(ctx, runner, setup)) + require.NoError(t, SetupCredentials(ctx, runner, setup)) + helper, err := CredentialHelper(instance, executable, "", "") + require.NoError(t, err) + data, err := runner.Output(ctx, cwd, false, "config", "--local", "--null", "--get-all", key) + require.NoError(t, err) + require.Equal(t, "\x00"+helper+"\x00", data) + fill := exec.Command("git", "credential", "fill") + fill.Dir = cwd + fill.Stdin = strings.NewReader("protocol=https\nhost=example.test:8443\npath=gitea/alice/project.git\n\n") + output, err := fill.Output() + require.NoError(t, err) + require.Contains(t, string(output), "username=tea\npassword=tea-token\n") +} + func TestSetupGitCredentialsWorksWithoutAnExistingGlobalFile(t *testing.T) { home := t.TempDir() t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(home, "new.gitconfig"))