Skip to content

Commit f4a8589

Browse files
committed
refactor git auth and support netrc auth
1 parent 58feae3 commit f4a8589

File tree

2 files changed

+163
-126
lines changed

2 files changed

+163
-126
lines changed

.secrets.baseline

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2025-11-28T15:02:26Z",
6+
"generated_at": "2025-12-01T14:11:15Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -225,26 +225,34 @@
225225
],
226226
"common/git.go": [
227227
{
228-
"hashed_secret": "bff8f8143a073833d713e3c1821fe97661bc3cef",
228+
"hashed_secret": "4896f3d2180fc0783f7f8987d5f7363cac93cb88",
229229
"is_secret": false,
230230
"is_verified": false,
231-
"line_number": 430,
231+
"line_number": 191,
232232
"type": "Secret Keyword",
233233
"verified_result": null
234234
},
235235
{
236236
"hashed_secret": "b4e929aa58c928e3e44d12e6f873f39cd8207a25",
237237
"is_secret": false,
238238
"is_verified": false,
239-
"line_number": 575,
239+
"line_number": 488,
240240
"type": "Secret Keyword",
241241
"verified_result": null
242242
},
243243
{
244-
"hashed_secret": "b994b23302ebc7b46888b0f5c623bfc2bcfa2e3f",
244+
"hashed_secret": "85634d4b50e1251589936fe11ec324888ae6c348",
245245
"is_secret": false,
246246
"is_verified": false,
247-
"line_number": 588,
247+
"line_number": 701,
248+
"type": "Secret Keyword",
249+
"verified_result": null
250+
},
251+
{
252+
"hashed_secret": "96f4a798dd800c119b9d2f327e0fd2f8fbea24ec",
253+
"is_secret": false,
254+
"is_verified": false,
255+
"line_number": 736,
248256
"type": "Secret Keyword",
249257
"verified_result": null
250258
}

common/git.go

Lines changed: 149 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"errors"
66
"fmt"
77
"log"
8+
"net/url"
89
"os"
910
"os/exec"
11+
"path/filepath"
1012
"regexp"
1113
"strings"
1214
"testing"
@@ -16,7 +18,7 @@ import (
1618
"github.com/go-git/go-git/v5/plumbing"
1719
"github.com/go-git/go-git/v5/plumbing/object"
1820
"github.com/go-git/go-git/v5/plumbing/transport"
19-
"github.com/go-git/go-git/v5/plumbing/transport/http"
21+
gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
2022
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
2123
"github.com/go-git/go-git/v5/storage/memory"
2224
"github.com/gruntwork-io/terratest/modules/logger"
@@ -182,9 +184,11 @@ func (g *realGitOps) commitExistsInRemote(remoteURL, commitID string) (bool, err
182184
config.RefSpec(fmt.Sprintf("+refs/heads/*:refs/remotes/%s/*", tempRemoteName)),
183185
config.RefSpec(fmt.Sprintf("+refs/pull/*/head:refs/remotes/%s/pr/*", tempRemoteName)),
184186
}
187+
auth, _ := GitAutoAuth(remoteURL)
185188
err = tempRemote.Fetch(&git.FetchOptions{
186189
RemoteName: tempRemoteName,
187190
RefSpecs: refSpecs,
191+
Auth: auth,
188192
})
189193
if err != nil && err != git.NoErrAlreadyUpToDate {
190194
return false, fmt.Errorf("fetch failed: %w", err)
@@ -397,72 +401,6 @@ func getCurrentPrRepoAndBranch(git gitOps) (string, string, error) {
397401
return repoURL, branch, nil
398402
}
399403

400-
// DetermineAuthMethod determines the appropriate authentication method for a given repository URL.
401-
// The function supports both HTTPS and SSH-based repositories.
402-
//
403-
// For HTTPS repositories:
404-
// - It first checks if the GIT_TOKEN environment variable is set. If so, it uses this as the Personal Access Token (PAT).
405-
// - If the GIT_TOKEN environment variable is not set, no authentication is used for HTTPS repositories.
406-
//
407-
// For SSH repositories:
408-
// - It first checks if the SSH_PRIVATE_KEY environment variable is set. If so, it uses this as the SSH private key.
409-
// - If the SSH_PRIVATE_KEY environment variable is not set, it attempts to use the default SSH key located at ~/.ssh/id_rsa.
410-
// - If neither the environment variable nor the default key is available, no authentication is used for SSH repositories.
411-
//
412-
// Parameters:
413-
// - repoURL: The URL of the Git repository.
414-
//
415-
// Returns:
416-
// - An appropriate AuthMethod based on the repository URL and available credentials.
417-
// - An error if there's an issue parsing the SSH private key or if the private key cannot be cast to an ssh.Signer.
418-
func DetermineAuthMethod(repoURL string) (transport.AuthMethod, error) {
419-
var pat string
420-
var sshPrivateKey string
421-
if strings.HasPrefix(repoURL, "https://") {
422-
// Check for Personal Access Token (PAT) in environment variable
423-
envPat, exists := os.LookupEnv("GIT_TOKEN")
424-
if exists {
425-
pat = envPat
426-
}
427-
if pat != "" {
428-
return &http.BasicAuth{
429-
Username: "git", // This can be anything except an empty string
430-
Password: pat,
431-
}, nil
432-
}
433-
} else if strings.HasPrefix(repoURL, "git@") {
434-
// SSH authentication
435-
envSSHKey, exists := os.LookupEnv("SSH_PRIVATE_KEY")
436-
if exists {
437-
sshPrivateKey = envSSHKey
438-
}
439-
if sshPrivateKey == "" {
440-
// Attempt to use the default SSH key if none is provided
441-
defaultKeyPath := os.ExpandEnv("$HOME/.ssh/id_rsa")
442-
if _, err := os.Stat(defaultKeyPath); !os.IsNotExist(err) {
443-
// Read the default key
444-
keyBytes, err := os.ReadFile(defaultKeyPath)
445-
if err != nil {
446-
return nil, err
447-
}
448-
sshPrivateKey = string(keyBytes)
449-
}
450-
}
451-
if sshPrivateKey != "" {
452-
key, err := RetrievePrivateKey(sshPrivateKey)
453-
if err != nil {
454-
return nil, err
455-
}
456-
signer, ok := key.(ssh.Signer)
457-
if !ok {
458-
return nil, errors.New("unable to cast private key to ssh.Signer")
459-
}
460-
return &gitssh.PublicKeys{User: "git", Signer: signer}, nil
461-
}
462-
}
463-
return nil, nil // No authentication
464-
}
465-
466404
// RetrievePrivateKey is a function that takes a string sshPvtKey as input and returns an interface{} and error as output.
467405
// IF the SSH_PASSPHRASE environment variable is set:
468406
// - It will parse the raw private key with passphrase using the ParseRawPrivateKeyWithPassphrase method of the ssh package.
@@ -541,64 +479,16 @@ func SkipUpgradeTest(testing *testing.T, source_repo string, source_branch strin
541479
}
542480

543481
func CloneAndCheckoutBranch(testing *testing.T, repoURL string, branch string, cloneDir string) error {
544-
545-
authMethod, authErr := DetermineAuthMethod(repoURL)
546-
if authErr != nil {
547-
logger.Log(testing, "Failed to determine authentication method, trying without authentication...")
548-
549-
// Convert SSH URL to HTTPS URL
550-
if strings.HasPrefix(repoURL, "git@") {
551-
repoURL = strings.Replace(repoURL, ":", "/", 1)
552-
repoURL = strings.Replace(repoURL, "git@", "https://", 1)
553-
repoURL = strings.TrimSuffix(repoURL, ".git") + ".git"
554-
}
555-
556-
// Try to clone without authentication
557-
_, errUnauth := git.PlainClone(cloneDir, false, &git.CloneOptions{
558-
URL: repoURL,
559-
ReferenceName: plumbing.NewBranchReferenceName(branch),
560-
SingleBranch: true,
561-
})
562-
563-
if errUnauth != nil {
564-
// If unauthenticated clone fails and we cannot determine authentication, return the error from the unauthenticated approach
565-
return fmt.Errorf("failed to determine authentication method and clone base repo and branch without authentication: %v", errUnauth)
566-
} else {
567-
logger.Log(testing, "Cloned base repo and branch without authentication")
568-
}
569-
} else {
570-
// Authentication method determined, try with authentication
571-
_, errAuth := git.PlainClone(cloneDir, false, &git.CloneOptions{
482+
authMethod, _ := GitAutoAuth(repoURL)
483+
if authMethod != nil {
484+
_, errClone := git.PlainClone(cloneDir, false, &git.CloneOptions{
572485
URL: repoURL,
573486
ReferenceName: plumbing.NewBranchReferenceName(branch),
574487
SingleBranch: true,
575488
Auth: authMethod,
576489
})
577-
578-
if errAuth != nil {
579-
logger.Log(testing, "Failed to clone base repo and branch with authentication, trying without authentication...")
580-
// Convert SSH URL to HTTPS URL
581-
if strings.HasPrefix(repoURL, "git@") {
582-
repoURL = strings.Replace(repoURL, ":", "/", 1)
583-
repoURL = strings.Replace(repoURL, "git@", "https://", 1)
584-
repoURL = strings.TrimSuffix(repoURL, ".git") + ".git"
585-
}
586-
587-
// Try to clone without authentication
588-
_, errUnauth := git.PlainClone(cloneDir, false, &git.CloneOptions{
589-
URL: repoURL,
590-
ReferenceName: plumbing.NewBranchReferenceName(branch),
591-
SingleBranch: true,
592-
})
593-
594-
if errUnauth != nil {
595-
// If unauthenticated clone also fails, return the error from the authenticated approach
596-
return fmt.Errorf("failed to clone base repo and branch with authentication: %v", errAuth)
597-
} else {
598-
logger.Log(testing, "Cloned base repo and branch without authentication")
599-
}
600-
} else {
601-
logger.Log(testing, "Cloned base repo and branch with authentication")
490+
if errClone != nil {
491+
return fmt.Errorf("failed to clone base repo and branch: %v", errClone)
602492
}
603493
}
604494

@@ -745,3 +635,142 @@ func getFileDiff(repoDir string, fileName string, git gitOps) (string, error) {
745635

746636
return string(diffOutput), nil
747637
}
638+
639+
// GitAutoAuth returns transport.AuthMethod for a remote URL (SSH or HTTPS)
640+
func GitAutoAuth(remoteURL string) (transport.AuthMethod, error) {
641+
if isSSHURL(remoteURL) {
642+
return sshAuth()
643+
}
644+
return httpsAuth(remoteURL)
645+
}
646+
647+
// SSH auth
648+
func sshAuth() (transport.AuthMethod, error) {
649+
// 1. Try SSH agent
650+
auth, err := gitssh.NewSSHAgentAuth("git")
651+
if err == nil {
652+
return auth, nil
653+
}
654+
655+
// 2. Try SSH_PRIVATE_KEY env variable
656+
keyData := os.Getenv("SSH_PRIVATE_KEY")
657+
if keyData != "" {
658+
auth, err := gitssh.NewPublicKeys("git", []byte(keyData), "")
659+
if err == nil {
660+
return auth, nil
661+
}
662+
}
663+
664+
// 3. Try default key file ~/.ssh/id_rsa
665+
home := os.Getenv("HOME")
666+
if home != "" {
667+
defaultKey := filepath.Join(home, ".ssh", "id_rsa")
668+
if _, err := os.Stat(defaultKey); err == nil {
669+
auth, err := gitssh.NewPublicKeysFromFile("git", defaultKey, "")
670+
if err == nil {
671+
return auth, nil
672+
}
673+
}
674+
}
675+
return nil, errors.New(
676+
"SSH authentication failed: no keys found. " +
677+
"Please start ssh-agent with loaded keys, set SSH_PRIVATE_KEY, or ensure ~/.ssh/id_rsa exists.",
678+
)
679+
}
680+
681+
func isSSHURL(raw string) bool {
682+
return strings.HasPrefix(raw, "git@") ||
683+
strings.HasPrefix(raw, "ssh://")
684+
}
685+
686+
// HTTPS auth
687+
func httpsAuth(remoteURL string) (transport.AuthMethod, error) {
688+
// Try .netrc first
689+
if auth := loadNetrcAuth(remoteURL); auth != nil {
690+
return auth, nil
691+
}
692+
693+
// Try common environment tokens
694+
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
695+
return &gitHttp.BasicAuth{Username: "token", Password: tok}, nil
696+
}
697+
if tok := os.Getenv("GITLAB_TOKEN"); tok != "" {
698+
return &gitHttp.BasicAuth{Username: "token", Password: tok}, nil
699+
}
700+
if tok := os.Getenv("BITBUCKET_TOKEN"); tok != "" {
701+
return &gitHttp.BasicAuth{Username: "token", Password: tok}, nil
702+
}
703+
704+
// Fallback: anonymous HTTPS
705+
return nil, nil
706+
}
707+
708+
// --------------------------
709+
// .netrc support
710+
// --------------------------
711+
type netrcMachine struct {
712+
Machine string
713+
Login string
714+
Password string
715+
}
716+
717+
func loadNetrcAuth(remoteURL string) *gitHttp.BasicAuth {
718+
home := os.Getenv("HOME")
719+
if home == "" {
720+
return nil
721+
}
722+
723+
netrcPath := filepath.Join(home, ".netrc")
724+
machines, err := parseNetrcFile(netrcPath)
725+
if err != nil {
726+
return nil
727+
}
728+
729+
m := lookupNetrcMachine(remoteURL, machines)
730+
if m == nil {
731+
return nil
732+
}
733+
734+
return &gitHttp.BasicAuth{
735+
Username: m.Login,
736+
Password: m.Password,
737+
}
738+
}
739+
740+
func lookupNetrcMachine(remoteURL string, machines []netrcMachine) *netrcMachine {
741+
u, err := url.Parse(remoteURL)
742+
if err != nil {
743+
return nil
744+
}
745+
746+
host := u.Hostname()
747+
748+
for _, m := range machines {
749+
if m.Machine == host {
750+
return &m
751+
}
752+
}
753+
return nil
754+
}
755+
756+
func parseNetrcFile(path string) ([]netrcMachine, error) {
757+
data, err := os.ReadFile(path)
758+
if err != nil {
759+
return nil, err
760+
}
761+
762+
words := strings.Fields(string(data))
763+
machines := []netrcMachine{}
764+
765+
for i := 0; i < len(words); i++ {
766+
if words[i] == "machine" && i+5 < len(words) {
767+
machines = append(machines, netrcMachine{
768+
Machine: words[i+1],
769+
Login: words[i+3],
770+
Password: words[i+5],
771+
})
772+
}
773+
}
774+
775+
return machines, nil
776+
}

0 commit comments

Comments
 (0)