diff --git a/retriever/git/retriever.go b/retriever/git/retriever.go index e2b11f7..68a0fac 100644 --- a/retriever/git/retriever.go +++ b/retriever/git/retriever.go @@ -77,6 +77,34 @@ func NewWithOptions(options *NewGitOptions) *Git { } methods = append(methods, NewBasicAuth(creds)) } + + if len(options.AuthOptions.tokens) > 0 { + // add duplicates after the general batch + var extraMethods []Authenticator + creds := make(map[string]Credential, len(options.AuthOptions.tokens)) + for host, tokens := range options.AuthOptions.tokens { + creds[host] = Credential{ + Username: "modv2", + Password: tokens[0], + } + // BasicAuth can only handle a single token for each host, so use the same map for each host + // In the cases where we have more than 1 token for a host create extra BasicAuth map for each + if len(tokens) > 1 { + for _, token := range tokens[1:] { + extraMethods = append(extraMethods, NewBasicAuth( + map[string]Credential{ + host: { + Username: "modv2", + Password: token, + }, + }, + )) + } + } + } + methods = append(methods, NewBasicAuth(creds)) + methods = append(methods, extraMethods...) + } } methods = append(methods, None{}) @@ -99,14 +127,55 @@ func NewWithOptions(options *NewGitOptions) *Git { type AuthOptions struct { // Credentials is a key-value pairs of , , e.g. { "github.com": {"username": "abcdef", "password": "123456"} } Credentials map[string]Credential - // Tokens is a key-value pairs of , , e.g. { "github.com": "qwerty" } + // Deprecated: Use WithTokens, WithTokenPairs or WithTokensFromString instead. Tokens map[string]string + // tokens is a map of key-value pairs of to list of s, e.g. { "github.com": ["qwerty"] } + // use WithTokensFromString() to populate it + tokens map[string][]string // SSHKeys is a key-value pairs of , , e.g. { "github.com": {"private_key": "~/.ssh/id_rsa_github", "private_key_password": ""} } SSHKeys map[string]SSHKey // True if authentication to a local repository should be included in the available methods. Local bool } +// WithTokens populates AuthOptions with the passed in tokens +func (ao *AuthOptions) WithTokens(t map[string][]string) *AuthOptions { + ao.tokens = t + return ao +} + +// WithTokenPairs populates AuthOptions with the passed in tokens pairs +func (ao *AuthOptions) WithTokenPairs(t map[string]string) *AuthOptions { + tokens := make(map[string][]string) + for host, token := range t { + tokens[host] = []string{token} + } + ao.tokens = tokens + return ao +} + +// WithTokensFromString populates AuthOptions with tokens parsed from a token string in the format 'hosta:,hostb:' +func (ao *AuthOptions) WithTokensFromString(tokensStr string) (*AuthOptions, error) { + if len(tokensStr) > 0 { + tokens := make(map[string][]string) + hostTokens := strings.Split(tokensStr, ",") + for _, t := range hostTokens { + arr := strings.Split(t, ":") + if len(arr) != 2 { + return nil, fmt.Errorf( + "token string is invalid, should be in format `hosta:,hostb:`: %s", + tokensStr, + ) + } + tokens[arr[0]] = append(tokens[arr[0]], arr[1]) + } + + ao.tokens = tokens + } + + return ao, nil +} + type SetOpts struct { Fetch OptFetch // How to fetch (or not) content from remote repositories. Reset OptReset // How to reset (or not) the state of repositories. diff --git a/retriever/git/retriever_test.go b/retriever/git/retriever_test.go index 255241b..ec86af8 100644 --- a/retriever/git/retriever_test.go +++ b/retriever/git/retriever_test.go @@ -3,6 +3,7 @@ package git import ( "context" "errors" + "fmt" "os" "testing" @@ -164,6 +165,7 @@ func TestGitRetrievePrivateRepoAuthNone(t *testing.T) { noneGit := New(nil) c, err := noneGit.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) + require.Error(t, err) errMsg := err.Error() require.Contains(t, errMsg, "git clone: Unable to authenticate, tried:") require.Contains(t, errMsg, "- None: authentication required") @@ -181,13 +183,51 @@ func TestGitRetrievePrivateRepoAuthToken(t *testing.T) { defer patch.Unpatch() - tokenGit := New(&AuthOptions{Tokens: map[string]string{"github.com": password}}) + tokenGit := New((&AuthOptions{}).WithTokenPairs(map[string]string{"github.com": password})) c, err := tokenGit.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) require.NoError(t, err) require.Equal(t, privRepoContent, string(c)) - wrongTokenGit := New(&AuthOptions{Tokens: map[string]string{"github.com": "foobar"}}) + wrongTokenGit := New((&AuthOptions{}).WithTokenPairs(map[string]string{"github.com": "foobar"})) c, err = wrongTokenGit.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) + require.Error(t, err) + errMsg := err.Error() + require.Contains(t, errMsg, "git clone: Unable to authenticate, tried:") + require.Contains(t, errMsg, "- None: authentication required") + require.Contains(t, errMsg, "- Username and Password/Token: authentication required") + require.Equal(t, "", string(c)) +} + +func TestGitRetrievePrivateRepoAuthMultipleTokens(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + patch, _ := mpatch.PatchMethod(NewSSHAgent, func() (*SSHAgent, error) { + return nil, errors.New("Create SSH Agent failed") + }) + + defer patch.Unpatch() + + authGoodFirst, err := (&AuthOptions{}).WithTokensFromString(fmt.Sprintf("github.com:%s,github.com:%s", password, "badToken")) + require.NoError(t, err) + tokenGitFirst := New(authGoodFirst) + c, err := tokenGitFirst.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) + require.NoError(t, err) + require.Equal(t, privRepoContent, string(c)) + + authBadFirst, err := (&AuthOptions{}).WithTokensFromString(fmt.Sprintf("github.com:%s,github.com:%s", "badToken", password)) + require.NoError(t, err) + tokenGitSecond := New(authBadFirst) + c, err = tokenGitSecond.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) + require.NoError(t, err) + require.Equal(t, privRepoContent, string(c)) + + authBothBad, err := (&AuthOptions{}).WithTokensFromString(fmt.Sprintf("github.com:%s,github.com:%s", "badToken1", "badToken2")) + require.NoError(t, err) + wrongTokenGit := New(authBothBad) + c, err = wrongTokenGit.Retrieve(context.Background(), ParseResource(t, privRepoREADME)) + require.Error(t, err) errMsg := err.Error() require.Contains(t, errMsg, "git clone: Unable to authenticate, tried:") require.Contains(t, errMsg, "- None: authentication required")