Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions pkg/git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ const (
defaultEmail = "pipecd.dev@gmail.com"
)

// basicAuthHeader builds the value of an HTTP "Authorization" header
// for basic authentication with the given username and password.
func basicAuthHeader(username, password string) string {
token := fmt.Sprintf("%s:%s", username, password)
encodedToken := base64.StdEncoding.EncodeToString([]byte(token))
return fmt.Sprintf("Authorization: Basic %s", encodedToken)
}

// Client is a git client for cloning/fetching git repo.
// It keeps a local cache for faster future cloning.
type Client interface {
Expand Down Expand Up @@ -148,9 +156,7 @@ func (c *client) Clone(ctx context.Context, repoID, remote, branch, destination
_, err, _ := c.repoSingleFlights.Do(repoID, func() (interface{}, error) {
authArgs := []string{}
if c.username != "" && c.password != "" {
token := fmt.Sprintf("%s:%s", c.username, c.password)
encodedToken := base64.StdEncoding.EncodeToString([]byte(token))
header := fmt.Sprintf("Authorization: Basic %s", encodedToken)
header := basicAuthHeader(c.username, c.password)
authArgs = append(authArgs, "-c", fmt.Sprintf("http.extraHeader=%s", header))
}

Expand Down Expand Up @@ -241,6 +247,14 @@ func (c *client) Clone(ctx context.Context, repoID, remote, branch, destination
return nil, fmt.Errorf("failed to set user: %v", err)
}
}
// Persist the basic auth header into the checked-out repo's git config so that
// subsequent git operations (e.g. pull, fetch, push) run directly against the
// remote continue to be authenticated, not just this initial clone.
if c.username != "" && c.password != "" {
if err := r.setHTTPAuthHeader(ctx, basicAuthHeader(c.username, c.password)); err != nil {
return nil, fmt.Errorf("failed to set up http auth: %v", err)
}
}

logger.Info("setting gc.autoDetach", zap.Bool("gc.autoDetach", c.gcAutoDetach))
if err := r.setGCAutoDetach(ctx, c.gcAutoDetach); err != nil {
Expand Down
38 changes: 38 additions & 0 deletions pkg/git/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -92,6 +93,43 @@ func TestClone(t *testing.T) {
assert.Equal(t, "Added note.txt", commits12[0].Message)
}

func TestClonePersistsHTTPAuthHeader(t *testing.T) {
faker, err := newFaker()
require.NoError(t, err)
defer faker.clean()

c, err := NewClient(WithUserName("git-user"), WithPassword("git-pass"))
require.NoError(t, err)
require.NotNil(t, c)
defer c.Clean()

err = faker.makeRepo("test-clone-org", "repo-auth")
require.NoError(t, err)

ctx := context.Background()
destPath, err := os.MkdirTemp("", "repo-auth-dest")
require.NoError(t, err)

repo, err := c.Clone(ctx, "repo-auth", filepath.Join(faker.dir, "test-clone-org/repo-auth"), "", destPath)
require.NoError(t, err)
require.NotNil(t, repo)
defer func() {
assert.NoError(t, repo.Clean())
}()

// Simulates what happens after the initial clone: a plain `git pull` run
// directly against the checked-out repo, as done by e.g. the event watcher.
// This must be able to find the credentials without any extra args, otherwise
// it fails with "could not read Username" for an HTTP(S) remote requiring auth.
cmd := exec.CommandContext(ctx, c.(*client).gitPath, "config", "--get", "http.extraHeader")
cmd.Dir = destPath
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))

wantHeader := basicAuthHeader("git-user", "git-pass")
assert.Equal(t, wantHeader, strings.TrimSuffix(string(out), "\n"))
}

type faker struct {
dir string
gitPath string
Expand Down
25 changes: 25 additions & 0 deletions pkg/git/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ type repo struct {
username string
email string

// httpAuthHeader is the HTTP "Authorization" header value used to authenticate
// with the remote when it's accessed over HTTP(S). It's empty when username/password
// based authentication isn't configured. It's set in the `setHTTPAuthHeader` method.
httpAuthHeader string

dir string
gitPath string
remote string
Expand Down Expand Up @@ -187,6 +192,14 @@ func (r *repo) CopyToModify(dest string) (Repo, error) {
}
}

// the cloned repo doesn't inherit custom config from the source one,
// so we need to set the http auth header again if it was configured.
if r.httpAuthHeader != "" {
if err := cloned.setHTTPAuthHeader(context.Background(), r.httpAuthHeader); err != nil {
return nil, fmt.Errorf("failed to set up http auth: %v", err)
}
}

// because we did a local cloning so set the remote url of origin
if err := cloned.setRemote(context.Background(), r.remote); err != nil {
return nil, err
Expand Down Expand Up @@ -441,6 +454,18 @@ func (r *repo) setUser(ctx context.Context, username, email string) error {
return nil
}

// setHTTPAuthHeader persists the given HTTP "Authorization" header value into the
// repo's git config, so that it's automatically included by git in every future
// git command run against this repo directory, not just the command it was
// originally issued with via a one-off `-c` flag.
func (r *repo) setHTTPAuthHeader(ctx context.Context, header string) error {
if out, err := r.runGitCommand(ctx, "config", "http.extraHeader", header); err != nil {
return formatCommandError(err, out)
}
r.httpAuthHeader = header
return nil
}

func (r *repo) setRemote(ctx context.Context, remote string) error {
out, err := r.runGitCommand(ctx, "remote", "set-url", "origin", remote)
if err != nil {
Expand Down
56 changes: 56 additions & 0 deletions pkg/git/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,49 @@ func Test_setGCAutoDetach(t *testing.T) {
assert.Equal(t, false, got)
}

func Test_setHTTPAuthHeader(t *testing.T) {
getHTTPExtraHeader := func(ctx context.Context, repo *repo) (string, error) {
cmd := exec.CommandContext(ctx, repo.gitPath, "config", "--get", "http.extraHeader")
cmd.Dir = repo.dir
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return strings.TrimSuffix(string(out), "\n"), nil
}

faker, err := newFaker()
require.NoError(t, err)
defer faker.clean()

var (
org = "test-repo-org"
repoName = "repo-set-http-auth-header"
ctx = context.Background()
)

err = faker.makeRepo(org, repoName)
require.NoError(t, err)

r := &repo{
dir: faker.repoDir(org, repoName),
gitPath: faker.gitPath,
}

// Before being set, the repo has no http.extraHeader configured.
_, err = getHTTPExtraHeader(ctx, r)
require.Error(t, err)

header := "Authorization: Basic dXNlcjpwYXNz"
err = r.setHTTPAuthHeader(ctx, header)
require.NoError(t, err)
assert.Equal(t, header, r.httpAuthHeader)

got, err := getHTTPExtraHeader(ctx, r)
require.NoError(t, err)
assert.Equal(t, header, got)
}

func TestCopy(t *testing.T) {
faker, err := newFaker()
require.NoError(t, err)
Expand Down Expand Up @@ -293,6 +336,10 @@ func TestCopyToModify(t *testing.T) {
remote: faker.repoDir(org, repoName), // use the same directory as remote, it's not a real remote. it's strange but it's ok for testing.
}

header := "Authorization: Basic dXNlcjpwYXNz"
err = r.setHTTPAuthHeader(ctx, header)
require.NoError(t, err)

commits, err := r.ListCommits(ctx, "")
require.NoError(t, err)
assert.Equal(t, 1, len(commits))
Expand All @@ -301,6 +348,15 @@ func TestCopyToModify(t *testing.T) {
newRepo, err := r.CopyToModify(tmpDir)
require.NoError(t, err)

// The http auth header must be propagated to the cloned repo too,
// since it's not carried over by a plain `git clone`.
assert.Equal(t, header, newRepo.(*repo).httpAuthHeader)
cmd := exec.CommandContext(ctx, r.gitPath, "config", "--get", "http.extraHeader")
cmd.Dir = tmpDir
out, err := cmd.CombinedOutput()
require.NoError(t, err)
assert.Equal(t, header, strings.TrimSuffix(string(out), "\n"))

// we can copy the repo to another directory multiple times
tmpDir2 := filepath.Join(faker.dir, "tmp-repo2")
newRepo2, err := r.CopyToModify(tmpDir2)
Expand Down
Loading