Skip to content

Commit

Permalink
Merge pull request #362 from carvel-dev/force-basic-auth
Browse files Browse the repository at this point in the history
Force basic auth via flag
  • Loading branch information
joaopapereira authored Feb 20, 2024
2 parents 7d0d6be + 0997e9e commit f7c3651
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 29 deletions.
1 change: 1 addition & 0 deletions pkg/vendir/config/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type DirectoryContentsGit struct {
DangerousSkipTLSVerify bool `json:"dangerousSkipTLSVerify,omitempty"`
SkipInitSubmodules bool `json:"skipInitSubmodules,omitempty"`
Depth int `json:"depth,omitempty"`
ForceHTTPBasicAuth bool `json:"forceHTTPBasicAuth,omitempty"`
}

type DirectoryContentsGitVerification struct {
Expand Down
81 changes: 52 additions & 29 deletions pkg/vendir/fetch/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package git

import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/url"
Expand All @@ -23,12 +24,20 @@ type Git struct {
opts ctlconf.DirectoryContentsGit
infoLog io.Writer
refFetcher ctlfetch.RefFetcher
cmdRunner CommandRunner
}

func NewGit(opts ctlconf.DirectoryContentsGit,
infoLog io.Writer, refFetcher ctlfetch.RefFetcher) *Git {

return &Git{opts, infoLog, refFetcher}
return &Git{opts, infoLog, refFetcher, &runner{infoLog}}
}

// NewGitWithRunner creates a Git retriever with a provided runner
func NewGitWithRunner(opts ctlconf.DirectoryContentsGit,
infoLog io.Writer, refFetcher ctlfetch.RefFetcher, cmdRunner CommandRunner) *Git {

return &Git{opts, infoLog, refFetcher, cmdRunner}
}

//nolint:revive
Expand All @@ -50,19 +59,19 @@ func (t *Git) Retrieve(dstPath string, tempArea ctlfetch.TempArea) (GitInfo, err

info := GitInfo{}

out, _, err := t.run([]string{"rev-parse", "HEAD"}, nil, dstPath)
out, _, err := t.cmdRunner.Run([]string{"rev-parse", "HEAD"}, nil, dstPath)
if err != nil {
return GitInfo{}, err
}

info.SHA = strings.TrimSpace(out)

out, _, err = t.run([]string{"describe", "--tags", info.SHA}, nil, dstPath)
out, _, err = t.cmdRunner.Run([]string{"describe", "--tags", info.SHA}, nil, dstPath)
if err == nil {
info.Tags = strings.Split(strings.TrimSpace(out), "\n")
}

out, _, err = t.run([]string{"log", "-n", "1", "--pretty=%B", info.SHA}, nil, dstPath)
out, _, err = t.cmdRunner.Run([]string{"log", "-n", "1", "--pretty=%B", info.SHA}, nil, dstPath)
if err != nil {
return GitInfo{}, err
}
Expand Down Expand Up @@ -127,31 +136,36 @@ func (t *Git) fetch(dstPath string, tempArea ctlfetch.TempArea) error {
gitURL := t.opts.URL
gitCredsPath := filepath.Join(authDir, ".git-credentials")

argss := [][]string{
{"init"},
{"config", "credential.helper", "store --file " + gitCredsPath},
{"remote", "add", "origin", gitURL},
}

if authOpts.Username != nil && authOpts.Password != nil {
if !strings.HasPrefix(gitURL, "https://") {
return fmt.Errorf("Username/password authentication is only supported for https remotes")
}

gitCredsURL, err := url.Parse(gitURL)
if err != nil {
return fmt.Errorf("Parsing git remote url: %s", err)
}
if t.opts.ForceHTTPBasicAuth {
encodedAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", *authOpts.Username, *authOpts.Password)))
argss = append(argss, []string{"config", "--add", "http.extraHeader", fmt.Sprintf("Authorization: Basic %s", encodedAuth)})
} else {
gitCredsURL, err := url.Parse(gitURL)
if err != nil {
return fmt.Errorf("Parsing git remote url: %s", err)
}

gitCredsURL.User = url.UserPassword(*authOpts.Username, *authOpts.Password)
gitCredsURL.Path = ""
gitCredsURL.User = url.UserPassword(*authOpts.Username, *authOpts.Password)
gitCredsURL.Path = ""

err = os.WriteFile(gitCredsPath, []byte(gitCredsURL.String()+"\n"), 0600)
if err != nil {
return fmt.Errorf("Writing %s: %s", gitCredsPath, err)
err = os.WriteFile(gitCredsPath, []byte(gitCredsURL.String()+"\n"), 0600)
if err != nil {
return fmt.Errorf("Writing %s: %s", gitCredsPath, err)
}
}
}

argss := [][]string{
{"init"},
{"config", "credential.helper", "store --file " + gitCredsPath},
{"remote", "add", "origin", gitURL},
}

argss = append(argss, []string{"config", "remote.origin.tagOpt", "--tags"})

{
Expand All @@ -166,7 +180,7 @@ func (t *Git) fetch(dstPath string, tempArea ctlfetch.TempArea) error {
argss = append(argss, fetchArgs)
}

err = t.runMultiple(argss, env, dstPath)
err = t.cmdRunner.RunMultiple(argss, env, dstPath)
if err != nil {
return err
}
Expand All @@ -183,13 +197,13 @@ func (t *Git) fetch(dstPath string, tempArea ctlfetch.TempArea) error {
}
}

_, _, err = t.run([]string{"-c", "advice.detachedHead=false", "checkout", ref}, env, dstPath)
_, _, err = t.cmdRunner.Run([]string{"-c", "advice.detachedHead=false", "checkout", ref}, env, dstPath)
if err != nil {
return err
}

if !t.opts.SkipInitSubmodules {
_, _, err = t.run([]string{"submodule", "update", "--init", "--recursive"}, env, dstPath)
_, _, err = t.cmdRunner.Run([]string{"submodule", "update", "--init", "--recursive"}, env, dstPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -219,34 +233,43 @@ func (t *Git) resolveRef(dstPath string) (string, error) {
}

func (t *Git) tags(dstPath string) ([]string, error) {
out, _, err := t.run([]string{"tag", "-l"}, nil, dstPath)
out, _, err := t.cmdRunner.Run([]string{"tag", "-l"}, nil, dstPath)
if err != nil {
return nil, err
}

return strings.Split(out, "\n"), nil
}

func (t *Git) runMultiple(argss [][]string, env []string, dstPath string) error {
type CommandRunner interface {
RunMultiple(argss [][]string, env []string, dstPath string) error
Run(args []string, env []string, dstPath string) (string, string, error)
}

type runner struct {
infoLog io.Writer
}

func (r *runner) RunMultiple(argss [][]string, env []string, dstPath string) error {
for _, args := range argss {
_, _, err := t.run(args, env, dstPath)
_, _, err := r.Run(args, env, dstPath)
if err != nil {
return err
}
}
return nil
}

func (t *Git) run(args []string, env []string, dstPath string) (string, string, error) {
func (r *runner) Run(args []string, env []string, dstPath string) (string, string, error) {
var stdoutBs, stderrBs bytes.Buffer

cmd := exec.Command("git", args...)
cmd.Env = env
cmd.Dir = dstPath
cmd.Stdout = io.MultiWriter(t.infoLog, &stdoutBs)
cmd.Stderr = io.MultiWriter(t.infoLog, &stderrBs)
cmd.Stdout = io.MultiWriter(r.infoLog, &stdoutBs)
cmd.Stderr = io.MultiWriter(r.infoLog, &stderrBs)

t.infoLog.Write([]byte(fmt.Sprintf("--> git %s\n", strings.Join(args, " "))))
r.infoLog.Write([]byte(fmt.Sprintf("--> git %s\n", strings.Join(args, " "))))

err := cmd.Run()
if err != nil {
Expand Down
94 changes: 94 additions & 0 deletions pkg/vendir/fetch/git/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

package git_test

import (
"os"
"strings"
"testing"

"carvel.dev/vendir/pkg/vendir/config"
"carvel.dev/vendir/pkg/vendir/fetch"
"carvel.dev/vendir/pkg/vendir/fetch/git"
"github.com/stretchr/testify/require"
)

func TestGit_Retrieve(t *testing.T) {
t.Run("Force basic auth header when flag is enabled and the user/pass is provided", func(t *testing.T) {
secretFetcher := &fetch.SingleSecretRefFetcher{Secret: &config.Secret{
Metadata: config.GenericMetadata{
Name: "some-secret",
},
Data: map[string][]byte{
"username": []byte("YWRtaW4="), // admin
"password": []byte("cGFzc3dvcmQ="), // password
},
}}
runner := &cmdRunnerLocal{commandsToRun: [][]string{}}
gitRetriever := git.NewGitWithRunner(config.DirectoryContentsGit{
URL: "https://some.git/repo",
Ref: "origin/main",
SecretRef: &config.DirectoryContentsLocalRef{Name: "some-secret"},
ForceHTTPBasicAuth: true,
}, os.Stdout, secretFetcher, runner)
_, err := gitRetriever.Retrieve("", &tmpFolder{t})
require.NoError(t, err)
isPresent := false
// Check that the header was added with the correct values
for _, args := range runner.commandsToRun {
if args[0] == "config" && args[1] == "--add" {
isPresent = true
require.Equal(t, "config --add http.extraHeader Authorization: Basic WVdSdGFXND06Y0dGemMzZHZjbVE9", strings.Join(args, " "))
}
}
require.True(t, isPresent, "could not find the configuration")
})

t.Run("Errors when authenticating with user/pass on http URL", func(t *testing.T) {
secretFetcher := &fetch.SingleSecretRefFetcher{Secret: &config.Secret{
Metadata: config.GenericMetadata{
Name: "some-secret",
},
Data: map[string][]byte{
"username": []byte("YWRtaW4="), // admin
"password": []byte("cGFzc3dvcmQ="), // password
},
}}
runner := &cmdRunnerLocal{commandsToRun: [][]string{}}
gitRetriever := git.NewGitWithRunner(config.DirectoryContentsGit{
URL: "http://some.git/repo",
Ref: "origin/main",
SecretRef: &config.DirectoryContentsLocalRef{Name: "some-secret"},
ForceHTTPBasicAuth: true,
}, os.Stdout, secretFetcher, runner)
_, err := gitRetriever.Retrieve("", &tmpFolder{t})
require.ErrorContains(t, err, "Username/password authentication is only supported for https remotes")
})
}

type cmdRunnerLocal struct {
commandsToRun [][]string
}

func (c *cmdRunnerLocal) RunMultiple(argss [][]string, _ []string, _ string) error {
c.commandsToRun = append(c.commandsToRun, argss...)
return nil
}

func (c *cmdRunnerLocal) Run(args []string, _ []string, _ string) (string, string, error) {
c.commandsToRun = append(c.commandsToRun, args)
return "", "", nil
}

type tmpFolder struct {
t *testing.T
}

func (t tmpFolder) NewTempDir(_ string) (string, error) {
return t.t.TempDir(), nil
}

func (t tmpFolder) NewTempFile(_ string) (*os.File, error) {
panic("Not implemented")
}

0 comments on commit f7c3651

Please sign in to comment.