Skip to content

Commit 8f19824

Browse files
authored
feat: support netrc auth and refactor git auth (#1084)
* refactor git auth and support netrc auth * allow flow of anonymous auth * added tests and debug messages * support GIT_TOKEN env var + debugs * clean up debugging
1 parent 9c89404 commit 8f19824

File tree

3 files changed

+324
-150
lines changed

3 files changed

+324
-150
lines changed

.secrets.baseline

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,26 +225,44 @@
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": 487,
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": 705,
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": 740,
256+
"type": "Secret Keyword",
257+
"verified_result": null
258+
}
259+
],
260+
"common/git_test.go": [
261+
{
262+
"hashed_secret": "8a75a804b061840e90a060962261c0dde61f54ef",
263+
"is_secret": false,
264+
"is_verified": false,
265+
"line_number": 421,
248266
"type": "Secret Keyword",
249267
"verified_result": null
250268
}

common/git.go

Lines changed: 159 additions & 126 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,65 +479,15 @@ 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{
572-
URL: repoURL,
573-
ReferenceName: plumbing.NewBranchReferenceName(branch),
574-
SingleBranch: true,
575-
Auth: authMethod,
576-
})
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")
602-
}
482+
authMethod, _ := GitAutoAuth(repoURL)
483+
_, errClone := git.PlainClone(cloneDir, false, &git.CloneOptions{
484+
URL: repoURL,
485+
ReferenceName: plumbing.NewBranchReferenceName(branch),
486+
SingleBranch: true,
487+
Auth: authMethod,
488+
})
489+
if errClone != nil {
490+
return fmt.Errorf("failed to clone base repo and branch: %v", errClone)
603491
}
604492

605493
return nil
@@ -745,3 +633,148 @@ func getFileDiff(repoDir string, fileName string, git gitOps) (string, error) {
745633

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

0 commit comments

Comments
 (0)