github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/auth/shared/git_credential.go (about)

     1  package shared
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/MakeNowJust/heredoc"
    12  	"github.com/ungtb10d/cli/v2/git"
    13  	"github.com/ungtb10d/cli/v2/internal/ghinstance"
    14  	"github.com/google/shlex"
    15  )
    16  
    17  type GitCredentialFlow struct {
    18  	Executable string
    19  	Prompter   Prompt
    20  	GitClient  *git.Client
    21  
    22  	shouldSetup bool
    23  	helper      string
    24  	scopes      []string
    25  }
    26  
    27  func (flow *GitCredentialFlow) Prompt(hostname string) error {
    28  	var gitErr error
    29  	flow.helper, gitErr = gitCredentialHelper(flow.GitClient, hostname)
    30  	if isOurCredentialHelper(flow.helper) {
    31  		flow.scopes = append(flow.scopes, "workflow")
    32  		return nil
    33  	}
    34  
    35  	result, err := flow.Prompter.Confirm("Authenticate Git with your GitHub credentials?", true)
    36  	if err != nil {
    37  		return err
    38  	}
    39  	flow.shouldSetup = result
    40  
    41  	if flow.shouldSetup {
    42  		if isGitMissing(gitErr) {
    43  			return gitErr
    44  		}
    45  		flow.scopes = append(flow.scopes, "workflow")
    46  	}
    47  
    48  	return nil
    49  }
    50  
    51  func (flow *GitCredentialFlow) Scopes() []string {
    52  	return flow.scopes
    53  }
    54  
    55  func (flow *GitCredentialFlow) ShouldSetup() bool {
    56  	return flow.shouldSetup
    57  }
    58  
    59  func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
    60  	return flow.gitCredentialSetup(hostname, username, authToken)
    61  }
    62  
    63  func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
    64  	gitClient := flow.GitClient
    65  	ctx := context.Background()
    66  
    67  	if flow.helper == "" {
    68  		credHelperKeys := []string{
    69  			gitCredentialHelperKey(hostname),
    70  		}
    71  
    72  		gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
    73  		if strings.HasPrefix(gistHost, "gist.") {
    74  			credHelperKeys = append(credHelperKeys, gitCredentialHelperKey(gistHost))
    75  		}
    76  
    77  		var configErr error
    78  
    79  		for _, credHelperKey := range credHelperKeys {
    80  			if configErr != nil {
    81  				break
    82  			}
    83  			// first use a blank value to indicate to git we want to sever the chain of credential helpers
    84  			preConfigureCmd, err := gitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "")
    85  			if err != nil {
    86  				configErr = err
    87  				break
    88  			}
    89  			if _, err = preConfigureCmd.Output(); err != nil {
    90  				configErr = err
    91  				break
    92  			}
    93  
    94  			// second configure the actual helper for this host
    95  			configureCmd, err := gitClient.Command(ctx,
    96  				"config", "--global", "--add",
    97  				credHelperKey,
    98  				fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
    99  			)
   100  			if err != nil {
   101  				configErr = err
   102  			} else {
   103  				_, configErr = configureCmd.Output()
   104  			}
   105  		}
   106  
   107  		return configErr
   108  	}
   109  
   110  	// clear previous cached credentials
   111  	rejectCmd, err := gitClient.Command(ctx, "credential", "reject")
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
   117  		protocol=https
   118  		host=%s
   119  	`, hostname))
   120  
   121  	_, err = rejectCmd.Output()
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	approveCmd, err := gitClient.Command(ctx, "credential", "approve")
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
   132  		protocol=https
   133  		host=%s
   134  		username=%s
   135  		password=%s
   136  	`, hostname, username, password))
   137  
   138  	_, err = approveCmd.Output()
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func gitCredentialHelperKey(hostname string) string {
   147  	host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/")
   148  	return fmt.Sprintf("credential.%s.helper", host)
   149  }
   150  
   151  func gitCredentialHelper(gitClient *git.Client, hostname string) (helper string, err error) {
   152  	ctx := context.Background()
   153  	helper, err = gitClient.Config(ctx, gitCredentialHelperKey(hostname))
   154  	if helper != "" {
   155  		return
   156  	}
   157  	helper, err = gitClient.Config(ctx, "credential.helper")
   158  	return
   159  }
   160  
   161  func isOurCredentialHelper(cmd string) bool {
   162  	if !strings.HasPrefix(cmd, "!") {
   163  		return false
   164  	}
   165  
   166  	args, err := shlex.Split(cmd[1:])
   167  	if err != nil || len(args) == 0 {
   168  		return false
   169  	}
   170  
   171  	return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
   172  }
   173  
   174  func isGitMissing(err error) bool {
   175  	if err == nil {
   176  		return false
   177  	}
   178  	var errNotInstalled *git.NotInstalled
   179  	return errors.As(err, &errNotInstalled)
   180  }
   181  
   182  func shellQuote(s string) string {
   183  	if strings.ContainsAny(s, " $\\") {
   184  		return "'" + s + "'"
   185  	}
   186  	return s
   187  }