package sync import ( "bytes" "encoding/base64" "log" "os" "os/exec" "path/filepath" "strings" "testing" ) // R-616: the catalog credentials must never be stored in the clone. A fake https remote is served from // a local repository through git's own url..insteadOf (in a test-only HOME), so the real clone // and fetch paths run with no network: git REWRITES the URL it dials but STORES the URL it was given — // which is exactly the URL the product chose, credentialed or not. func r616Fixture(t *testing.T) (s *Syncer, logs *bytes.Buffer, src string) { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git not on PATH") } root := t.TempDir() src = filepath.Join(root, "src") run := func(dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } home := filepath.Join(root, "home") os.MkdirAll(home, 0o755) t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) t.Setenv("GIT_CONFIG_NOSYSTEM", "1") gitcfg := "[user]\n\tname = t\n\temail = t@example.invalid\n" + "[url \"file://" + src + "\"]\n" + "\tinsteadOf = https://user:tok-SECRET@catalog.example.invalid/admin/catalog.git\n" + "\tinsteadOf = https://catalog.example.invalid/admin/catalog.git\n" if err := os.WriteFile(filepath.Join(home, ".gitconfig"), []byte(gitcfg), 0o644); err != nil { t.Fatal(err) } os.MkdirAll(src, 0o755) run(src, "init", "-q", "-b", "main") os.WriteFile(filepath.Join(src, "README"), []byte("x\n"), 0o644) run(src, "add", "README") run(src, "commit", "-q", "-m", "init") s = newTestSyncer(t, "https://catalog.example.invalid/admin/catalog.git") s.cfg.Git.Username = "user" s.cfg.Git.Token = "tok-SECRET" logs = &bytes.Buffer{} s.logger = log.New(logs, "", 0) return s, logs, src } func storedOrigin(t *testing.T, dir string) string { t.Helper() out, err := exec.Command("git", "-C", dir, "config", "--get", "remote.origin.url").Output() if err != nil { t.Fatalf("read origin: %v", err) } return strings.TrimSpace(string(out)) } func TestR616_CloneStoresNoCredentials(t *testing.T) { s, logs, _ := r616Fixture(t) if err := s.gitCloneOrPull(); err != nil { t.Fatalf("clone: %v", err) } origin := storedOrigin(t, s.cacheDir) if strings.Contains(origin, "@") || strings.Contains(origin, "tok-SECRET") { t.Errorf("the clone's stored origin carries credentials: %q", origin) } cfgBytes, _ := os.ReadFile(filepath.Join(s.cacheDir, ".git", "config")) if strings.Contains(string(cfgBytes), "tok-SECRET") { t.Errorf(".git/config holds the token in the clear:\n%s", cfgBytes) } // A pull (fetch + reset) still works on the clean clone. if err := s.gitCloneOrPull(); err != nil { t.Fatalf("pull: %v", err) } if strings.Contains(logs.String(), "tok-SECRET") { t.Errorf("the token reached the log:\n%s", logs.String()) } } // A clone made BEFORE the fix (origin with userinfo) is scrubbed on the next pull. func TestR616_PreFixCloneIsScrubbedOnPull(t *testing.T) { s, logs, _ := r616Fixture(t) cmd := exec.Command("git", "clone", "-q", "--depth", "1", "--branch", "main", "https://user:tok-SECRET@catalog.example.invalid/admin/catalog.git", s.cacheDir) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("pre-fix clone: %v\n%s", err, out) } if !strings.Contains(storedOrigin(t, s.cacheDir), "tok-SECRET") { t.Fatal("fixture: the pre-fix clone should carry the token") } if err := s.gitCloneOrPull(); err != nil { t.Fatalf("pull: %v", err) } if got := storedOrigin(t, s.cacheDir); got != "https://catalog.example.invalid/admin/catalog.git" { t.Errorf("stored origin not scrubbed: %q", got) } if strings.Contains(logs.String(), "tok-SECRET") { t.Errorf("the token reached the log:\n%s", logs.String()) } } // The credentials still reach git: the header is scoped to the remote's origin and carries the same // Basic pair the URL form sent. Checked with git's own URL matcher, no network. func TestR616_AuthHeaderScopedToTheRemote(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not on PATH") } s := newTestSyncer(t, "https://catalog.example.invalid/admin/catalog.git") if env := s.gitAuthEnv(); env != nil { t.Errorf("no credentials configured -> no auth env, got %v", env) } s.cfg.Git.Username, s.cfg.Git.Token = "user", "tok-SECRET" env := s.gitAuthEnv() want := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte("user:tok-SECRET")) match := func(url string) string { cmd := exec.Command("git", "config", "--get-urlmatch", "http.extraHeader", url) cmd.Env = append(os.Environ(), env...) out, _ := cmd.Output() return strings.TrimSpace(string(out)) } t.Setenv("GIT_CONFIG_NOSYSTEM", "1") t.Setenv("HOME", t.TempDir()) if got := match("https://catalog.example.invalid/admin/catalog.git"); got != want { t.Errorf("header for the remote = %q, want the Basic pair", got) } if got := match("https://other.example.invalid/x.git"); got != "" { t.Errorf("the header must not reach another host, got %q", got) } s.cfg.Git.RepoURL = "/local/path/catalog" if env := s.gitAuthEnv(); env != nil { t.Errorf("a non-https remote gets no auth env, got %v", env) } } // R-616 × R-615: with a token set and the SAME repository configured, repeated syncs never re-clone // (the configured URL carries no credentials to differ from the stored origin) and the stored origin // stays free of '@'. A re-clone is detected by a marker file planted inside the cache: RemoveAll would // take it with it. func TestR616_TokenSetSameRepoNoRecloneOriginClean(t *testing.T) { s, logs, _ := r616Fixture(t) if err := s.gitCloneOrPull(); err != nil { t.Fatalf("clone: %v", err) } marker := filepath.Join(s.cacheDir, ".git", "r616-no-reclone") if err := os.WriteFile(marker, []byte("x"), 0o644); err != nil { t.Fatal(err) } for i := 0; i < 3; i++ { if err := s.gitCloneOrPull(); err != nil { t.Fatalf("pull %d: %v", i, err) } } if _, err := os.Stat(marker); err != nil { t.Error("the cache was RE-CLONED on a same-repo sync with a token set") } if o := storedOrigin(t, s.cacheDir); strings.Contains(o, "@") { t.Errorf("stored origin carries credentials: %q", o) } if strings.Contains(logs.String(), "re-cloning") || strings.Contains(logs.String(), "tok-SECRET") { t.Errorf("unexpected re-clone or token in the log:\n%s", logs.String()) } }