Skip to content

Commit f020ad9

Browse files
authored
Omit access tokens from Git errors (#28)
1 parent 8caa718 commit f020ad9

File tree

7 files changed

+129
-19
lines changed

7 files changed

+129
-19
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
./services
1+
services

pkg/avancement/service_manager.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package avancement
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"log"
78
"net/url"
@@ -97,7 +98,7 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
9798
} else {
9899
source, errorSource = s.checkoutSourceRepo(fromURL, fromBranch)
99100
if errorSource != nil {
100-
return fmt.Errorf("failed to checkout repo: %w", errorSource)
101+
return git.GitError("error checking out source repository from Git", fromURL)
101102
}
102103
reposToDelete = append(reposToDelete, source)
103104
if newBranchName == "" {
@@ -107,15 +108,19 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
107108

108109
destination, err := s.checkoutDestinationRepo(toURL, newBranchName)
109110
if err != nil {
110-
return fmt.Errorf("failed to checkout repo: %w", err)
111+
if git.IsGitError(err) {
112+
return git.GitError(fmt.Sprintf("failed to clone destination repository, error: %s", err.Error()), toURL)
113+
}
114+
// This would be a checkout error as the clone error gives us the above gitError instead
115+
return fmt.Errorf("failed to checkout destination repository, error: %w", err)
111116
}
112117
reposToDelete = append(reposToDelete, destination)
113118

114119
var copied []string
115120
if isLocal {
116121
copied, err = local.CopyConfig(serviceName, localSource, destination)
117122
if err != nil {
118-
return fmt.Errorf("failed to setup local repo: %w", err)
123+
return fmt.Errorf("failed to set up local repository: %w", err)
119124
}
120125
} else {
121126
copied, err = git.CopyService(serviceName, source, destination)
@@ -141,13 +146,14 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
141146
}
142147

143148
if err := destination.Push(newBranchName); err != nil {
144-
return fmt.Errorf("failed to push: %w", err)
149+
return fmt.Errorf("failed to push to Git repository - check the access token is correct with sufficient permissions: %w", err)
145150
}
146151

147152
ctx := context.Background()
148153
pr, err := createPullRequest(ctx, fromURL, toURL, newBranchName, commitMsg, s.clientFactory(s.author.Token), isLocal)
149154
if err != nil {
150-
return fmt.Errorf("failed to create a pull-request for branch %s in %v: %w", newBranchName, toURL, err)
155+
message := fmt.Sprintf("failed to create a pull-request for branch %s, error: %s", newBranchName, err)
156+
return git.GitError(message, toURL)
151157
}
152158
log.Printf("created PR %d", pr.Number)
153159
return nil
@@ -156,23 +162,26 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
156162
func (s *ServiceManager) checkoutSourceRepo(repoURL, branch string) (git.Repo, error) {
157163
repo, err := s.cloneRepo(repoURL, branch)
158164
if err != nil {
159-
return nil, fmt.Errorf("failed to clone source repo %s: %w", repoURL, err)
165+
if git.IsGitError(err) {
166+
return nil, git.GitError("failed to clone source repository", repoURL)
167+
}
168+
return nil, err
160169
}
161170
err = repo.Checkout(branch)
162171
if err != nil {
163-
return nil, fmt.Errorf("failed to checkout branch %s in repo %s: %w", branch, repoURL, err)
172+
return nil, git.GitError(fmt.Sprintf("failed to checkout branch %s, error: %s", branch, err.Error()), repoURL)
164173
}
165174
return repo, nil
166175
}
167176

168177
func (s *ServiceManager) checkoutDestinationRepo(repoURL, branch string) (git.Repo, error) {
169178
repo, err := s.cloneRepo(repoURL, branch)
170179
if err != nil {
171-
return nil, fmt.Errorf("failed to clone destination repo %s: %w", repoURL, err)
180+
return nil, git.GitError(err.Error(), repoURL)
172181
}
173182
err = repo.CheckoutAndCreate(branch)
174183
if err != nil {
175-
return nil, fmt.Errorf("failed to checkout branch %s in repo %s: %w", branch, repoURL, err)
184+
return nil, fmt.Errorf("failed to checkout branch %s: %w", branch, err)
176185
}
177186
return repo, nil
178187
}
@@ -185,7 +194,8 @@ func (s *ServiceManager) cloneRepo(repoURL, branch string) (git.Repo, error) {
185194
}
186195
repo, err := s.repoFactory(repoURL, path.Join(s.cacheDir, encode(repoURL, branch)), s.debug)
187196
if err != nil {
188-
return nil, fmt.Errorf("failed to create repo for URL %s: %w", repoURL, err)
197+
message := fmt.Sprintf("failed to clone repository, error is: %s", err.Error())
198+
return nil, git.GitError(message, repoURL)
189199
}
190200
err = repo.Clone()
191201
if err != nil {
@@ -205,7 +215,7 @@ func createPullRequest(ctx context.Context, fromURL, toURL, newBranchName, commi
205215
// TODO: improve this error message
206216
return nil, err
207217
}
208-
// TODO: come up with a better way of generating the repo URL (this
218+
// TODO: come up with a better way of generating the repository URL (this
209219
// only works for GitHub)
210220
pr, _, err := client.PullRequests.Create(ctx, fmt.Sprintf("%s/%s", user, repo), prInput)
211221
return pr, err
@@ -217,8 +227,11 @@ func encode(gitURL, branch string) string {
217227

218228
func addCredentialsIfNecessary(s string, a *git.Author) (string, error) {
219229
parsed, err := url.Parse(s)
230+
// If this does error (e.g. if there's a leading ":" character") one would see
231+
// parse ":[email protected]": missing protocol scheme
232+
// thus surfacing the token. So intentionally display a generic parse problem
220233
if err != nil {
221-
return "", fmt.Errorf("failed to parse git repo url %v: %w", s, err)
234+
return "", errors.New("failed to parse git repository url")
222235
}
223236
if parsed.Scheme != "https" || parsed.User != nil {
224237
return s, nil

pkg/avancement/service_manager_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
fakescm "github.com/jenkins-x/go-scm/scm/driver/fake"
1414
"github.com/rhd-gitops-example/services/pkg/git"
1515
"github.com/rhd-gitops-example/services/pkg/git/mock"
16+
"github.com/rhd-gitops-example/services/test"
1617
)
1718

1819
const (
@@ -281,3 +282,26 @@ func (s *mockSource) AddFiles(name string) {
281282
}
282283
s.files = append(s.files, path.Join(s.localPath, name))
283284
}
285+
286+
func TestRepositoryCloneErrorOmitsToken(t *testing.T) {
287+
dstBranch := "test-branch"
288+
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
289+
client, _ := fakescm.NewDefault()
290+
fakeClientFactory := func(s string) *scm.Client {
291+
return client
292+
}
293+
sm := New("tmp", author)
294+
sm.clientFactory = fakeClientFactory
295+
296+
sm.repoFactory = func(url, _ string, _ bool) (git.Repo, error) {
297+
// This actually causes the error and results in trying to create a repository
298+
// which can surface the token
299+
errorMessage := fmt.Errorf("failed to clone repository %s: exit status 128", dev)
300+
return nil, errorMessage
301+
}
302+
err := sm.Promote("my-service", dev, staging, dstBranch, "", false)
303+
if err != nil {
304+
devRepoToUseInError := fmt.Sprintf(".*%s", dev)
305+
test.AssertErrorMatch(t, devRepoToUseInError, err)
306+
}
307+
}

pkg/git/errors.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package git
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
)
8+
9+
// We can choose to throw one of these errors if we want to handle it
10+
// in a particularly special way. This will be useful for Git API calls where
11+
// we know there's going to be a repository URL involved and it's not
12+
// some other error (e.g. to do with file permissions)
13+
type gitError struct {
14+
msg string
15+
}
16+
17+
// GitError returns an error whereby the user part of the url is removed (e.g. an access token)
18+
// Accepts the message to use and the url to remove the user part from
19+
func GitError(msg, url string) error {
20+
cleanedURL, err := CleanURL(url)
21+
if err != nil {
22+
return err
23+
}
24+
return gitError{msg: fmt.Sprintf("%s. repository URL = %s", msg, cleanedURL)}
25+
}
26+
27+
// IsGitError performs a cast to see if an error really is of type gitError
28+
func IsGitError(err error) bool {
29+
_, ok := err.(gitError)
30+
return ok
31+
}
32+
33+
// CleanURL removes the .User part of the string (typically an access token)
34+
func CleanURL(inputURL string) (string, error) {
35+
parsedURL, parseErr := url.Parse(inputURL)
36+
if parseErr != nil {
37+
// Intentionally don't display any real parse error as it would contain any token
38+
return "", errors.New("failed to parse git repository URL, check it's well-formed")
39+
}
40+
parsedURL.User = nil
41+
return parsedURL.String(), nil
42+
}
43+
44+
func (e gitError) Error() string {
45+
return e.msg
46+
}

pkg/git/errors_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package git
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestCleanURLRemovesTokenFromError(t *testing.T) {
9+
url := "https://[email protected]/my-repo"
10+
cleanedURL, err := CleanURL(url)
11+
if err != nil {
12+
t.Fatal("got an error cleaning a well-formed URL")
13+
}
14+
if strings.Contains(cleanedURL, "mytoken") {
15+
t.Fatal("error still contains access token")
16+
}
17+
}
18+
19+
func TestCleanURLThrowsErrorIfInvalidURL(t *testing.T) {
20+
url := ":_ https://[email protected]/my-repo"
21+
cleanedURL, err := CleanURL(url)
22+
if err == nil {
23+
t.Fatalf("did not get an an error cleaning a bad URL, cleaned URL is: %s", cleanedURL)
24+
}
25+
}

pkg/git/repository.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package git
22

33
import (
44
"bytes"
5+
"errors"
56
"fmt"
67
"io"
78
"log"
@@ -45,8 +46,8 @@ func (r *Repository) Clone() error {
4546
if err != nil {
4647
return fmt.Errorf("error creating the cache dir %s: %w", r.LocalPath, err)
4748
}
48-
out, err := r.execGit(r.LocalPath, nil, "clone", r.RepoURL)
49-
log.Printf("%s\n", out)
49+
// Intentionally omit output as it can contain an access token
50+
_, err = r.execGit(r.LocalPath, nil, "clone", r.RepoURL)
5051
return err
5152
}
5253

@@ -144,11 +145,12 @@ func (r *Repository) execGit(workingDir string, env []string, args ...string) ([
144145
func repoName(u string) (string, error) {
145146
parsed, err := url.Parse(u)
146147
if err != nil {
147-
return "", fmt.Errorf("failed to parse URL '%s': %w", u, err)
148+
// Don't surface the URL as it could contain a token
149+
return "", errors.New("failed to parse the URL when determining repository name")
148150
}
149151
parts := strings.Split(parsed.Path, "/")
150152
if len(parts) < 3 {
151-
return "", fmt.Errorf("could not identify repo: %s", u)
153+
return "", fmt.Errorf("could not identify repository name: %s", u)
152154
}
153155
return strings.TrimSuffix(parts[len(parts)-1], ".git"), nil
154156
}

pkg/git/repository_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestRepoName(t *testing.T) {
2424
wantErr string
2525
}{
2626
{testRepository, "staging", ""},
27-
{"https://github.com/mnuttall", "", "could not identify repo: https://github.com/mnuttall"},
27+
{"https://github.com/mnuttall", "", "could not identify repository name: https://github.com/mnuttall"},
2828
}
2929

3030
for _, tt := range nameTests {
@@ -259,7 +259,7 @@ func TestDebugEnabled(t *testing.T) {
259259
r, err := NewRepository(testRepository, path.Join(tempDir, "path"), true)
260260
assertNoError(t, err)
261261
if !r.debug {
262-
t.Fatalf("Debug not set to true")
262+
t.Fatalf("Debug not set to true")
263263
}
264264
}
265265

0 commit comments

Comments
 (0)